packages feed

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

{-# LANGUAGE DerivingStrategies #-}

module Moonlight.Homology.Pure.Topology.Morse
  ( MorsePivotOps (..),
    intUnitMorsePivotOps,
    integerUnitMorsePivotOps,
    rationalMorsePivotOps,
    gf2MorsePivotOps,
    AlgebraicMorsePair,
    AlgebraicMorseMatching,
    AlgebraicMorseComplex,
    AcyclicPair (..),
    IntegralAcyclicPair,
    LocalizedAcyclicPair (..),
    RationalAcyclicPair,
    CollapseObstruction (..),
    LocalizedCollapseObstruction (..),
    AcyclicMatching (..),
    LocalizedAcyclicMatching (..),
    MorseComplex (..),
    LocalizedMorseComplex (..),
    RefinedMatchingStage,
    RefinedAcyclicMatching,
    RefinedMorseComplex (..),
    FilteredMorsePairWitness (..),
    FilteredMorseCompatibility (..),
    FilteredRefinedMorseComplex (..),
    RefinedMatchingSummary (..),
    acyclicMatching,
    acyclicMatchingLocalized,
    refinedAcyclicMatchingTranscript,
    refinedMorseComplex,
    filteredRefinedMorseComplex,
    reducedFiltrationByCriticalBasis,
    filteredReducedFiltration,
    rationalizeFiniteChainComplex,
    foldRefinedAcyclicMatching,
    traverseRefinedStages,
    mapRefinedStages,
    summarizeRefinedMatching,
    refinedMatchingSummary,
    refinedStageCount,
    hasRefinedStages,
    isTerminalRefinedMatching,
    finalRefinedCriticalDegrees,
    finalRefinedCriticalCellCount,
    finalRefinedCriticalDegreeHistogram,
    finalRefinedHomologicalSupport,
    finalRefinedMaxCriticalDegree,
    refinedMatchingCriticalCells,
    refinedStageMatching,
    refinedStageReducedComplex,
    refinedStageCriticalBasis,
    flattenRefinedAcyclicMatching,
    refinedAcyclicMatching,
    acyclicMatchingWith,
    morseComplexWith,
    isAcyclicMatchingWith,
    extractCandidatePairsWith,
    reverseCandidateEdgeWith,
    morseComplex,
    morseComplexLocalized,
    isAcyclicMatching,
    isAcyclicMatchingLocalized,
    extractCandidatePairsLocalized,
    reverseCandidateEdgeLocalized,
  )
where

import Data.Function ((&))
import Data.Kind (Type)
import Data.List (sortOn)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Set qualified as Set
import Moonlight.Core (Semiring)
import Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    incidenceMatrixAt,
    maxHomologicalDegree,
    mkFiniteChainComplex,
    validateFiniteChainComplexShape,
  )
import Moonlight.Homology.Boundary.LinAlg
  ( boundaryCoefficient,
    boundaryEntries,
    emptyBoundaryIncidence,
    mapBoundaryCoefficients,
    materializeIncidenceBoundary,
    sourceIndex,
    targetIndex,
  )
import Moonlight.Homology.Pure.Chain (HomologicalDegree (..))
import Moonlight.Homology.Pure.Failure (HomologyFailure (..), HomologyLaw (..))
import Moonlight.Homology.Pure.LinearCombination qualified as LC
import Moonlight.Homology.Pure.Reductions
  ( ChainHomotopy (..),
    ChainMap (..),
  )
import Moonlight.Homology.Pure.Carrier (BasisCellRef (..))
import Moonlight.Homology.Pure.Topology.Core (allBasisCellRefs)
import Moonlight.Homology.Pure.Topology.Morse.Digraph
  ( DirectedAdjacencyMap,
    DirectedEdgeMap,
    PathWeightOracle,
    adjacencyMap,
    basisCellDimension,
    basisCellKey,
    deleteAdjacencyEdge,
    graphIsAcyclic,
    insertAdjacencyEdge,
    pathWeightOracle,
    pathWeightsFromOracle,
    topologicalOrderWithVertices,
  )
import Moonlight.LinAlg.Pure.Dense.GF2 (GF2 (..))

type MorsePivotOps :: Type -> Type
newtype MorsePivotOps r = MorsePivotOps
  { mpoUnitInverse :: r -> Maybe r
  }

intUnitMorsePivotOps :: MorsePivotOps Int
intUnitMorsePivotOps = unitMorsePivotOps

integerUnitMorsePivotOps :: MorsePivotOps Integer
integerUnitMorsePivotOps = unitMorsePivotOps

rationalMorsePivotOps :: MorsePivotOps Rational
rationalMorsePivotOps =
  MorsePivotOps
    { mpoUnitInverse =
        \coefficientValue ->
          if coefficientValue == 0
            then Nothing
            else Just (recip coefficientValue)
    }

gf2MorsePivotOps :: MorsePivotOps GF2
gf2MorsePivotOps =
  MorsePivotOps
    { mpoUnitInverse =
        \coefficientValue ->
          case coefficientValue of
            GF2Zero -> Nothing
            GF2One -> Just GF2One
    }

unitMorsePivotOps :: (Eq r, Num r) => MorsePivotOps r
unitMorsePivotOps =
  MorsePivotOps
    { mpoUnitInverse =
        \coefficientValue ->
          case coefficientValue of
            1 -> Just 1
            -1 -> Just (-1)
            _ -> Nothing
    }

type AcyclicPair :: Type
data AcyclicPair = AcyclicPair
  { apLowerCell :: BasisCellRef,
    apUpperCell :: BasisCellRef,
    apIncidenceCoefficient :: Int
  }
  deriving stock (Eq, Show)

type IntegralAcyclicPair :: Type
type IntegralAcyclicPair = AcyclicPair

type LocalizedAcyclicPair :: Type -> Type
data LocalizedAcyclicPair r = LocalizedAcyclicPair
  { lapLowerCell :: BasisCellRef,
    lapUpperCell :: BasisCellRef,
    lapIncidenceCoefficient :: r
  }
  deriving stock (Eq, Show)

type RationalAcyclicPair :: Type
type RationalAcyclicPair = LocalizedAcyclicPair Rational

type AlgebraicMorsePair :: Type -> Type
type AlgebraicMorsePair = LocalizedAcyclicPair

type CollapseObstruction :: Type
data CollapseObstruction = CollapseObstruction
  { coCandidate :: AcyclicPair,
    coCycleWitness :: [BasisCellRef]
  }
  deriving stock (Eq, Show)

type LocalizedCollapseObstruction :: Type -> Type
data LocalizedCollapseObstruction pair = LocalizedCollapseObstruction
  { lcoCandidate :: pair,
    lcoCycleWitness :: [BasisCellRef]
  }
  deriving stock (Eq, Show)

type AcyclicMatching :: Type
data AcyclicMatching = AcyclicMatching
  { amPairs :: [AcyclicPair],
    amCriticalCells :: [BasisCellRef],
    amObstructions :: [CollapseObstruction]
  }
  deriving stock (Eq, Show)

type LocalizedAcyclicMatching :: Type -> Type
data LocalizedAcyclicMatching pair = LocalizedAcyclicMatching
  { lamPairs :: [pair],
    lamCriticalCells :: [BasisCellRef],
    lamObstructions :: [LocalizedCollapseObstruction pair]
  }
  deriving stock (Eq, Show)

type AlgebraicMorseMatching :: Type -> Type
type AlgebraicMorseMatching r = LocalizedAcyclicMatching (AlgebraicMorsePair r)

type MorseComplex :: Type -> Type
data MorseComplex r = MorseComplex
  { mcMatching :: AcyclicMatching,
    mcReducedComplex :: FiniteChainComplex r,
    mcCriticalBasis :: Map BasisCellRef BasisCellRef,
    mcProjection :: ChainMap BasisCellRef BasisCellRef r,
    mcInclusion :: ChainMap BasisCellRef BasisCellRef r,
    mcHomotopy :: ChainHomotopy BasisCellRef r
  }

type LocalizedMorseComplex :: Type -> Type
data LocalizedMorseComplex r = LocalizedMorseComplex
  { lmcMatching :: LocalizedAcyclicMatching (LocalizedAcyclicPair r),
    lmcReducedComplex :: FiniteChainComplex r,
    lmcCriticalBasis :: Map BasisCellRef BasisCellRef,
    lmcProjection :: ChainMap BasisCellRef BasisCellRef r,
    lmcInclusion :: ChainMap BasisCellRef BasisCellRef r,
    lmcHomotopy :: ChainHomotopy BasisCellRef r
  }

type AlgebraicMorseComplex :: Type -> Type
type AlgebraicMorseComplex = LocalizedMorseComplex

type RefinedMatchingStage :: Type -> Type
data RefinedMatchingStage r = RefinedMatchingStage
  { rmsMatching :: LocalizedAcyclicMatching (LocalizedAcyclicPair r),
    rmsReducedComplex :: Maybe (FiniteChainComplex r),
    rmsCriticalBasis :: Maybe (Map BasisCellRef BasisCellRef)
  }

type RefinedAcyclicMatching :: Type -> Type
data RefinedAcyclicMatching r = RefinedAcyclicMatching
  { ramStages :: [RefinedMatchingStage r],
    ramCriticalCells :: [BasisCellRef]
  }

type RefinedMorseComplex :: Type -> Type
data RefinedMorseComplex r = RefinedMorseComplex
  { rmcTranscript :: RefinedAcyclicMatching r,
    rmcReducedComplex :: FiniteChainComplex r,
    rmcCriticalBasis :: Map BasisCellRef BasisCellRef
  }

type FilteredMorsePairWitness :: Type
data FilteredMorsePairWitness = FilteredMorsePairWitness
  { fmpwLowerCell :: BasisCellRef,
    fmpwUpperCell :: BasisCellRef,
    fmpwFiltrationLevel :: Int
  }
  deriving stock (Eq, Show)

type FilteredMorseCompatibility :: Type
newtype FilteredMorseCompatibility = FilteredMorseCompatibility
  { fmcPairWitnesses :: [FilteredMorsePairWitness]
  }
  deriving stock (Eq, Show)

type FilteredRefinedMorseComplex :: Type -> Type
data FilteredRefinedMorseComplex r = FilteredRefinedMorseComplex
  { frmcRefinedMorseComplex :: RefinedMorseComplex r,
    frmcReducedFiltrationByBasis :: Map BasisCellRef Int,
    frmcCompatibility :: FilteredMorseCompatibility
  }

type RefinedMorseDescent :: Type -> Type
data RefinedMorseDescent r
  = RefinedMorseDescentComplete (RefinedMorseComplex r)
  | RefinedMorseDescentBlocked (RefinedAcyclicMatching r) HomologyFailure

type RefinedMatchingSummary :: Type
data RefinedMatchingSummary = RefinedMatchingSummary
  { rmsStageCount :: Int,
    rmsHasStages :: Bool,
    rmsIsTerminal :: Bool,
    rmsFinalCriticalCellCount :: Int,
    rmsFinalCriticalDegreeHistogram :: Map HomologicalDegree Int,
    rmsFinalHomologicalSupport :: Set.Set HomologicalDegree,
    rmsFinalMaxCriticalDegree :: Maybe HomologicalDegree
  }
  deriving stock (Eq, Show)

type GradientAcyclicState :: Type -> Type
data GradientAcyclicState edgeWeight = GradientAcyclicState
  { gasEdgeMap :: !(DirectedEdgeMap edgeWeight),
    gasAdjacency :: !DirectedAdjacencyMap,
    gasMatchedUpperByLower :: !(Map BasisCellRef BasisCellRef),
    gasGradientAdjacency :: !DirectedAdjacencyMap
  }

type AcyclicRewrite :: Type -> Type
data AcyclicRewrite edgeWeight
  = AcyclicRewriteAccepted (GradientAcyclicState edgeWeight)
  | AcyclicRewriteRejected [BasisCellRef]

acyclicMatching ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  AcyclicMatching
acyclicMatching chainComplex cellScore =
  integralMatchingFromAlgebraic
    ( acyclicMatchingFromEdgeMapWith
        integerUnitMorsePivotOps
        (const True)
        (allBasisCellRefs chainComplex)
        (integralBoundaryEdgeMap chainComplex)
        cellScore
    )

acyclicMatchingWith ::
  (Eq r, Num r) =>
  MorsePivotOps r ->
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  AlgebraicMorseMatching r
acyclicMatchingWith pivotOps chainComplex =
  acyclicMatchingFromEdgeMapWith
    pivotOps
    (const True)
    (allBasisCellRefs chainComplex)
    (boundaryEdgeMap chainComplex)

acyclicMatchingLocalized ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  LocalizedAcyclicMatching RationalAcyclicPair
acyclicMatchingLocalized chainComplex cellScore =
  acyclicMatchingWith rationalMorsePivotOps (rationalizeFiniteChainComplex chainComplex) cellScore

refinedAcyclicMatching ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  Either HomologyFailure (LocalizedAcyclicMatching RationalAcyclicPair)
{-# DEPRECATED refinedAcyclicMatching "Use refinedAcyclicMatchingTranscript for canonical stage-aware reductions; use fmap flattenRefinedAcyclicMatching only as a compatibility projection." #-}
refinedAcyclicMatching chainComplex cellScore =
  flattenRefinedAcyclicMatching <$> refinedAcyclicMatchingTranscript chainComplex cellScore

refinedAcyclicMatchingTranscript ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  Either HomologyFailure (RefinedAcyclicMatching Rational)
refinedAcyclicMatchingTranscript chainComplex cellScore =
  case runRefinedMorseDescent chainComplex cellScore of
    RefinedMorseDescentComplete refinedComplex -> Right (rmcTranscript refinedComplex)
    RefinedMorseDescentBlocked _ failureValue -> Left failureValue

refinedMorseComplex ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  Either HomologyFailure (RefinedMorseComplex Rational)
refinedMorseComplex chainComplex cellScore =
  case runRefinedMorseDescent chainComplex cellScore of
    RefinedMorseDescentComplete refinedComplex -> Right refinedComplex
    RefinedMorseDescentBlocked _ failureValue -> Left failureValue

filteredRefinedMorseComplex ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Int) ->
  (BasisCellRef -> Double) ->
  Either HomologyFailure (FilteredRefinedMorseComplex Rational)
filteredRefinedMorseComplex chainComplex originalFiltration cellScore =
  case runFilteredRefinedMorseDescent chainComplex originalFiltration cellScore of
    RefinedMorseDescentBlocked _ failureValue -> Left failureValue
    RefinedMorseDescentComplete refinedComplex -> do
      reducedFiltrationValue <-
        reducedFiltrationByCriticalBasis
          (rmcReducedComplex refinedComplex)
          (rmcCriticalBasis refinedComplex)
          originalFiltration
      compatibilityValue <- filteredMorseCompatibility originalFiltration (rmcTranscript refinedComplex)
      Right
        FilteredRefinedMorseComplex
          { frmcRefinedMorseComplex = refinedComplex,
            frmcReducedFiltrationByBasis = reducedFiltrationValue,
            frmcCompatibility = compatibilityValue
          }

runRefinedMorseDescent ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  RefinedMorseDescent Rational
runRefinedMorseDescent chainComplex cellScore =
  runRefinedMorseDescentWith (\_ currentComplex currentScore -> acyclicMatchingRational currentComplex currentScore) chainComplex cellScore

runFilteredRefinedMorseDescent ::
  Integral r =>
  FiniteChainComplex r ->
  (BasisCellRef -> Int) ->
  (BasisCellRef -> Double) ->
  RefinedMorseDescent Rational
runFilteredRefinedMorseDescent chainComplex originalFiltration cellScore =
  runRefinedMorseDescentWith
    ( \currentBasis currentComplex currentScore ->
        acyclicMatchingRationalWith
          (filteredPairCompatible originalFiltration currentBasis)
          currentComplex
          currentScore
    )
    chainComplex
    cellScore

runRefinedMorseDescentWith ::
  Integral r =>
  ( Map BasisCellRef BasisCellRef ->
    FiniteChainComplex Rational ->
    (BasisCellRef -> Double) ->
    LocalizedAcyclicMatching RationalAcyclicPair
  ) ->
  FiniteChainComplex r ->
  (BasisCellRef -> Double) ->
  RefinedMorseDescent Rational
runRefinedMorseDescentWith matchingAt chainComplex cellScore =
  -- The first stage runs the fully gated 'morseComplexRational' (the user's
  -- complex and the initial matching are unproven input); every later stage
  -- operates on a theorem-guaranteed reduction with an engine-built matching
  -- and descends through the trusted entry — the invariant is established
  -- once at the boundary, not re-proved per stage.
  refineMatching morseComplexRational [] initialBasis initialComplex
  where
    initialComplex = rationalizeFiniteChainComplex chainComplex
    initialBasis = Map.fromList [(cell, cell) | cell <- allBasisCellRefs initialComplex]
    buildTranscript :: [RefinedMatchingStage Rational] -> [BasisCellRef] -> RefinedAcyclicMatching Rational
    buildTranscript accumulatedStages criticalCells =
      RefinedAcyclicMatching
        { ramStages = reverse accumulatedStages,
          ramCriticalCells = criticalCells
        }
    refineMatching ::
      ( FiniteChainComplex Rational ->
        LocalizedAcyclicMatching RationalAcyclicPair ->
        Either HomologyFailure (LocalizedMorseComplex Rational)
      ) ->
      [RefinedMatchingStage Rational] ->
      Map BasisCellRef BasisCellRef ->
      FiniteChainComplex Rational ->
      RefinedMorseDescent Rational
    refineMatching reduceStage accumulatedStages currentBasis currentComplex =
      let currentMatching = matchingAt currentBasis currentComplex (cellScore . rebaseCell currentBasis)
          currentPairs = lamPairs currentMatching
          translatedMatching = rebaseLocalizedMatching currentBasis currentMatching
          translatedCriticalCells = lamCriticalCells translatedMatching
       in case currentPairs of
            [] ->
              let transcriptValue = buildTranscript accumulatedStages translatedCriticalCells
               in RefinedMorseDescentComplete
                    RefinedMorseComplex
                      { rmcTranscript = transcriptValue,
                        rmcReducedComplex = currentComplex,
                        rmcCriticalBasis = currentBasis
                      }
            _ ->
              case reduceStage currentComplex currentMatching of
                Left failureValue ->
                  RefinedMorseDescentBlocked
                    ( buildTranscript
                        ( RefinedMatchingStage
                            { rmsMatching = translatedMatching,
                              rmsReducedComplex = Nothing,
                              rmsCriticalBasis = Nothing
                            } :
                          accumulatedStages
                        )
                        translatedCriticalCells
                    )
                    failureValue
                Right reducedComplexData ->
                  let nextReducedComplex = lmcReducedComplex reducedComplexData
                      nextCriticalBasis = composeCriticalBasis currentBasis (lmcCriticalBasis reducedComplexData)
                      stageValue =
                        RefinedMatchingStage
                          { rmsMatching = translatedMatching,
                            rmsReducedComplex = Just nextReducedComplex,
                            rmsCriticalBasis = Just nextCriticalBasis
                          }
                   in refineMatching
                        morseComplexRationalTrusted
                        (stageValue : accumulatedStages)
                        nextCriticalBasis
                        nextReducedComplex

filteredPairCompatible ::
  (BasisCellRef -> Int) ->
  Map BasisCellRef BasisCellRef ->
  RationalAcyclicPair ->
  Bool
filteredPairCompatible originalFiltration currentBasis candidatePair =
  let lowerOriginal = rebaseCell currentBasis (localizedLowerCell candidatePair)
      upperOriginal = rebaseCell currentBasis (localizedUpperCell candidatePair)
   in originalFiltration lowerOriginal == originalFiltration upperOriginal

reducedFiltrationByCriticalBasis ::
  FiniteChainComplex r ->
  Map BasisCellRef BasisCellRef ->
  (BasisCellRef -> Int) ->
  Either HomologyFailure (Map BasisCellRef Int)
reducedFiltrationByCriticalBasis reducedComplex criticalBasis originalFiltration =
  Map.fromList
    <$> traverse
      ( \basisRef ->
          fmap
            (\originalBasisRef -> (basisRef, originalFiltration originalBasisRef))
            (criticalOriginalBasis criticalBasis basisRef)
      )
      (allBasisCellRefs reducedComplex)

-- | Filtration value of a reduced basis cell. Cells outside the reduced
-- critical basis answer 0 by convention — callers that need to distinguish
-- "filtration 0" from "not a critical cell" should consult
-- 'frmcReducedFiltrationByBasis' directly.
filteredReducedFiltration ::
  FilteredRefinedMorseComplex r ->
  BasisCellRef ->
  Int
filteredReducedFiltration filteredComplex basisRef =
  Map.findWithDefault 0 basisRef (frmcReducedFiltrationByBasis filteredComplex)

criticalOriginalBasis ::
  Map BasisCellRef BasisCellRef ->
  BasisCellRef ->
  Either HomologyFailure BasisCellRef
criticalOriginalBasis criticalBasis basisRef =
  case Map.lookup basisRef criticalBasis of
    Just originalBasisRef -> Right originalBasisRef
    Nothing -> Left (MissingCriticalBasisProvenance basisRef)

filteredMorseCompatibility ::
  (BasisCellRef -> Int) ->
  RefinedAcyclicMatching Rational ->
  Either HomologyFailure FilteredMorseCompatibility
filteredMorseCompatibility originalFiltration transcriptValue =
  fmap
    (FilteredMorseCompatibility . foldMap id)
    (traverse (stageFilteredPairWitnesses originalFiltration) (ramStages transcriptValue))

stageFilteredPairWitnesses ::
  (BasisCellRef -> Int) ->
  RefinedMatchingStage Rational ->
  Either HomologyFailure [FilteredMorsePairWitness]
stageFilteredPairWitnesses originalFiltration stageValue =
  traverse
    (filteredPairWitness originalFiltration)
    (lamPairs (rmsMatching stageValue))

filteredPairWitness ::
  (BasisCellRef -> Int) ->
  RationalAcyclicPair ->
  Either HomologyFailure FilteredMorsePairWitness
filteredPairWitness originalFiltration candidatePair =
  let lowerCell = localizedLowerCell candidatePair
      upperCell = localizedUpperCell candidatePair
      lowerLevel = originalFiltration lowerCell
      upperLevel = originalFiltration upperCell
   in if lowerLevel == upperLevel
        then
          Right
            FilteredMorsePairWitness
              { fmpwLowerCell = lowerCell,
                fmpwUpperCell = upperCell,
                fmpwFiltrationLevel = lowerLevel
              }
        else
          Left (FiltrationIncompatibleMorsePair lowerCell upperCell lowerLevel upperLevel)

foldRefinedAcyclicMatching ::
  (RefinedMatchingStage r -> a -> a) ->
  ([BasisCellRef] -> a) ->
  RefinedAcyclicMatching r ->
  a
foldRefinedAcyclicMatching step finish refinedMatching =
  foldr step (finish (ramCriticalCells refinedMatching)) (ramStages refinedMatching)

traverseRefinedStages ::
  Applicative f =>
  (RefinedMatchingStage r -> f a) ->
  RefinedAcyclicMatching r ->
  f [a]
traverseRefinedStages summarizeStage =
  foldRefinedAcyclicMatching
    (\stageValue accumulatedSummaries -> (:) <$> summarizeStage stageValue <*> accumulatedSummaries)
    (const (pure []))

mapRefinedStages ::
  (RefinedMatchingStage r -> a) ->
  RefinedAcyclicMatching r ->
  [a]
mapRefinedStages summarizeStage =
  foldRefinedAcyclicMatching
    (\stageValue accumulatedSummaries -> summarizeStage stageValue : accumulatedSummaries)
    (const [])

summarizeRefinedMatching ::
  Monoid a =>
  (RefinedMatchingStage r -> a) ->
  RefinedAcyclicMatching r ->
  (a, [BasisCellRef])
summarizeRefinedMatching summarizeStage =
  foldRefinedAcyclicMatching
    (\stageValue (stageSummary, criticalCells) -> (summarizeStage stageValue <> stageSummary, criticalCells))
    (\criticalCells -> (mempty, criticalCells))

refinedMatchingSummary :: RefinedAcyclicMatching r -> RefinedMatchingSummary
refinedMatchingSummary refinedMatching =
  RefinedMatchingSummary
    { rmsStageCount = refinedStageCount refinedMatching,
      rmsHasStages = hasRefinedStages refinedMatching,
      rmsIsTerminal = isTerminalRefinedMatching refinedMatching,
      rmsFinalCriticalCellCount = finalRefinedCriticalCellCount refinedMatching,
      rmsFinalCriticalDegreeHistogram = finalRefinedCriticalDegreeHistogram refinedMatching,
      rmsFinalHomologicalSupport = finalRefinedHomologicalSupport refinedMatching,
      rmsFinalMaxCriticalDegree = finalRefinedMaxCriticalDegree refinedMatching
    }

refinedStageCount :: RefinedAcyclicMatching r -> Int
refinedStageCount =
  foldRefinedAcyclicMatching (\_ countValue -> countValue + 1) (const 0)

hasRefinedStages :: RefinedAcyclicMatching r -> Bool
hasRefinedStages =
  foldRefinedAcyclicMatching (\_ _ -> True) (const False)

isTerminalRefinedMatching :: RefinedAcyclicMatching r -> Bool
isTerminalRefinedMatching =
  all (== HomologicalDegree 0) . finalRefinedCriticalDegrees

finalRefinedCriticalDegrees :: RefinedAcyclicMatching r -> [HomologicalDegree]
finalRefinedCriticalDegrees =
  fmap cellDegree . refinedMatchingCriticalCells

finalRefinedCriticalCellCount :: RefinedAcyclicMatching r -> Int
finalRefinedCriticalCellCount =
  length . refinedMatchingCriticalCells

finalRefinedCriticalDegreeHistogram :: RefinedAcyclicMatching r -> Map HomologicalDegree Int
finalRefinedCriticalDegreeHistogram =
  Map.fromListWith (+)
    . fmap (\degreeValue -> (degreeValue, 1))
    . finalRefinedCriticalDegrees

finalRefinedHomologicalSupport :: RefinedAcyclicMatching r -> Set.Set HomologicalDegree
finalRefinedHomologicalSupport =
  Set.fromList . finalRefinedCriticalDegrees

finalRefinedMaxCriticalDegree :: RefinedAcyclicMatching r -> Maybe HomologicalDegree
finalRefinedMaxCriticalDegree =
  Set.lookupMax . finalRefinedHomologicalSupport

refinedMatchingCriticalCells :: RefinedAcyclicMatching r -> [BasisCellRef]
refinedMatchingCriticalCells =
  foldRefinedAcyclicMatching (\_ criticalCells -> criticalCells) id

refinedStageMatching ::
  RefinedMatchingStage r ->
  LocalizedAcyclicMatching (LocalizedAcyclicPair r)
refinedStageMatching RefinedMatchingStage {rmsMatching = matching} = matching

refinedStageReducedComplex ::
  RefinedMatchingStage r ->
  Maybe (FiniteChainComplex r)
refinedStageReducedComplex RefinedMatchingStage {rmsReducedComplex = reducedComplex} = reducedComplex

refinedStageCriticalBasis ::
  RefinedMatchingStage r ->
  Maybe (Map BasisCellRef BasisCellRef)
refinedStageCriticalBasis RefinedMatchingStage {rmsCriticalBasis = criticalBasis} = criticalBasis

flattenRefinedAcyclicMatching ::
  RefinedAcyclicMatching r ->
  LocalizedAcyclicMatching (LocalizedAcyclicPair r)
flattenRefinedAcyclicMatching refinedMatching =
  foldRefinedAcyclicMatching
    (\stage accumulatedMatching ->
       LocalizedAcyclicMatching
         { lamPairs = lamPairs (rmsMatching stage) <> lamPairs accumulatedMatching,
           lamCriticalCells = lamCriticalCells accumulatedMatching,
           lamObstructions = lamObstructions (rmsMatching stage) <> lamObstructions accumulatedMatching
         }
    )
    (\criticalCells ->
       LocalizedAcyclicMatching
         { lamPairs = [],
           lamCriticalCells = criticalCells,
           lamObstructions = []
         }
    )
    refinedMatching

morseComplex ::
  (Integral r, Semiring r) =>
  FiniteChainComplex r ->
  AcyclicMatching ->
  Either HomologyFailure (MorseComplex r)
morseComplex chainComplex matching =
  case morseComplexWith unitMorsePivotOps chainComplex (integralMatchingToAlgebraic matching) of
    Left failureValue -> Left failureValue
    Right morseValue ->
      Right
        MorseComplex
          { mcMatching = matching,
            mcReducedComplex = lmcReducedComplex morseValue,
            mcCriticalBasis = lmcCriticalBasis morseValue,
            mcProjection = lmcProjection morseValue,
            mcInclusion = lmcInclusion morseValue,
            mcHomotopy = lmcHomotopy morseValue
          }

morseComplexWith ::
  (Eq r, Num r, Semiring r) =>
  MorsePivotOps r ->
  FiniteChainComplex r ->
  AlgebraicMorseMatching r ->
  Either HomologyFailure (AlgebraicMorseComplex r)
morseComplexWith pivotOps chainComplex matching
  | not (isAcyclicMatchingAgainstEdgeMapWith pivotOps allCells initialEdgeMap matching) =
      Left (InvalidTopologyInput "Invalid algebraic acyclic matching")
  | otherwise =
      case reductionData of
        Left failureValue -> Left failureValue
        Right reducedData ->
          Right
            LocalizedMorseComplex
              { lmcMatching = matching,
                lmcReducedComplex = mrdReducedComplex reducedData,
                lmcCriticalBasis = mrdCriticalBasis reducedData,
                lmcProjection = mrdProjection reducedData,
                lmcInclusion = mrdInclusion reducedData,
                lmcHomotopy = mrdHomotopy reducedData
              }
  where
    allCells = allBasisCellRefs chainComplex
    initialEdgeMap = boundaryEdgeMap chainComplex
    reducedMatchingPairs = lamPairs matching
    criticalCells = expectedCriticalCells localizedLowerCell localizedUpperCell allCells reducedMatchingPairs
    finalEdgeMap = foldl' (flip (reverseCandidateEdgeWith pivotOps)) initialEdgeMap reducedMatchingPairs
    reductionData =
      buildReducedComplexData chainComplex criticalCells reducedMatchingPairs initialEdgeMap finalEdgeMap

morseComplexLocalized ::
  Integral r =>
  FiniteChainComplex r ->
  LocalizedAcyclicMatching RationalAcyclicPair ->
  Either HomologyFailure (LocalizedMorseComplex Rational)
morseComplexLocalized chainComplex matching =
  morseComplexWith rationalMorsePivotOps (rationalizeFiniteChainComplex chainComplex) matching

isAcyclicMatching ::
  Integral r =>
  FiniteChainComplex r ->
  AcyclicMatching ->
  Bool
isAcyclicMatching chainComplex =
  isAcyclicMatchingWith unitMorsePivotOps chainComplex . integralMatchingToAlgebraic

isAcyclicMatchingWith ::
  (Eq r, Num r) =>
  MorsePivotOps r ->
  FiniteChainComplex r ->
  AlgebraicMorseMatching r ->
  Bool
isAcyclicMatchingWith pivotOps chainComplex matching =
  isAcyclicMatchingAgainstEdgeMapWith pivotOps (allBasisCellRefs chainComplex) (boundaryEdgeMap chainComplex) matching

isAcyclicMatchingAgainstEdgeMapWith ::
  (Eq r, Num r) =>
  MorsePivotOps r ->
  [BasisCellRef] ->
  DirectedEdgeMap r ->
  AlgebraicMorseMatching r ->
  Bool
isAcyclicMatchingAgainstEdgeMapWith pivotOps allCells initialEdgeMap matching =
  uniquePairEndpoints localizedLowerCell localizedUpperCell matchingPairs
    && all pairIsValid matchingPairs
    && Set.fromList (lamCriticalCells matching) == Set.fromList expectedCritical
    && graphIsAcyclic finalEdgeMap
  where
    matchingPairs = lamPairs matching
    expectedCritical = expectedCriticalCells localizedLowerCell localizedUpperCell allCells matchingPairs
    finalEdgeMap = foldl' (flip (reverseCandidateEdgeWith pivotOps)) initialEdgeMap matchingPairs
    pairIsValid candidatePair =
      let lowerCell = localizedLowerCell candidatePair
          upperCell = localizedUpperCell candidatePair
          coefficientValue = localizedIncidenceCoefficient candidatePair
       in isCodimensionOne lowerCell upperCell
            && maybe False (const True) (mpoUnitInverse pivotOps coefficientValue)
            && Map.lookup (upperCell, lowerCell) initialEdgeMap == Just coefficientValue

isAcyclicMatchingLocalized ::
  Integral r =>
  FiniteChainComplex r ->
  LocalizedAcyclicMatching RationalAcyclicPair ->
  Bool
isAcyclicMatchingLocalized chainComplex matching =
  isAcyclicMatchingWith rationalMorsePivotOps (rationalizeFiniteChainComplex chainComplex) matching

extractCandidatePairsWith :: MorsePivotOps r -> DirectedEdgeMap r -> [AlgebraicMorsePair r]
extractCandidatePairsWith pivotOps edgeMap =
  edgeMap
    & Map.toAscList
    & mapMaybe
      ( \((upperCell, lowerCell), coefficientValue) ->
          if maybe False (const True) (mpoUnitInverse pivotOps coefficientValue)
            then
              Just
                LocalizedAcyclicPair
                  { lapLowerCell = lowerCell,
                    lapUpperCell = upperCell,
                    lapIncidenceCoefficient = coefficientValue
                  }
            else Nothing
      )

extractCandidatePairsLocalized :: DirectedEdgeMap Rational -> [RationalAcyclicPair]
extractCandidatePairsLocalized =
  extractCandidatePairsWith rationalMorsePivotOps

reverseCandidateEdgeWith ::
  Num r =>
  MorsePivotOps r ->
  AlgebraicMorsePair r ->
  DirectedEdgeMap r ->
  DirectedEdgeMap r
reverseCandidateEdgeWith pivotOps candidatePair =
  case mpoUnitInverse pivotOps (localizedIncidenceCoefficient candidatePair) of
    Nothing -> id
    Just inverseCoefficient ->
      Map.delete (localizedUpperCell candidatePair, localizedLowerCell candidatePair)
        . Map.insert
          (localizedLowerCell candidatePair, localizedUpperCell candidatePair)
          (negate inverseCoefficient)

reverseCandidateEdgeLocalized ::
  RationalAcyclicPair ->
  DirectedEdgeMap Rational ->
  DirectedEdgeMap Rational
reverseCandidateEdgeLocalized =
  reverseCandidateEdgeWith rationalMorsePivotOps

acyclicMatchingRational ::
  FiniteChainComplex Rational ->
  (BasisCellRef -> Double) ->
  LocalizedAcyclicMatching RationalAcyclicPair
acyclicMatchingRational chainComplex cellScore =
  acyclicMatchingWith rationalMorsePivotOps chainComplex cellScore

acyclicMatchingRationalWith ::
  (RationalAcyclicPair -> Bool) ->
  FiniteChainComplex Rational ->
  (BasisCellRef -> Double) ->
  LocalizedAcyclicMatching RationalAcyclicPair
acyclicMatchingRationalWith pairAllowed chainComplex cellScore =
  acyclicMatchingFromEdgeMapWith
    rationalMorsePivotOps
    pairAllowed
    (allBasisCellRefs chainComplex)
    (boundaryEdgeMap chainComplex)
    cellScore

acyclicMatchingFromEdgeMapWith ::
  Num r =>
  MorsePivotOps r ->
  (AlgebraicMorsePair r -> Bool) ->
  [BasisCellRef] ->
  DirectedEdgeMap r ->
  (BasisCellRef -> Double) ->
  AlgebraicMorseMatching r
acyclicMatchingFromEdgeMapWith pivotOps pairAllowed allCells edgeMap cellScore =
  buildMatchingBy
    allCells
    cellScore
    edgeMap
    (filter pairAllowed (extractCandidatePairsWith pivotOps edgeMap))
    localizedLowerCell
    localizedUpperCell
    (reverseCandidateEdgeWith pivotOps)
    (\candidate witness -> LocalizedCollapseObstruction {lcoCandidate = candidate, lcoCycleWitness = witness})
    (\pairs criticalCells obstructions ->
       LocalizedAcyclicMatching
         { lamPairs = pairs,
           lamCriticalCells = criticalCells,
           lamObstructions = obstructions
         }
    )

morseComplexRational ::
  FiniteChainComplex Rational ->
  LocalizedAcyclicMatching RationalAcyclicPair ->
  Either HomologyFailure (LocalizedMorseComplex Rational)
morseComplexRational =
  morseComplexWith rationalMorsePivotOps

-- | Gate-free variant of 'morseComplexRational' for the refined descent's
-- interior stages: the stage complex is a Morse reduction of a complex the
-- first (gated) stage already validated, and the stage matching comes from
-- the internal greedy engine, which certifies acyclicity incrementally as it
-- accepts each pair. Re-running 'isAcyclicMatchingAgainstEdgeMapWith' and the
-- shape/nilpotence sweep per stage would re-prove established invariants.
morseComplexRationalTrusted ::
  FiniteChainComplex Rational ->
  LocalizedAcyclicMatching RationalAcyclicPair ->
  Either HomologyFailure (LocalizedMorseComplex Rational)
morseComplexRationalTrusted chainComplex matching =
  case buildReducedComplexDataTrusted chainComplex criticalCells reducedMatchingPairs finalEdgeMap of
    Left failureValue -> Left failureValue
    Right reducedData ->
      Right
        LocalizedMorseComplex
          { lmcMatching = matching,
            lmcReducedComplex = mrdReducedComplex reducedData,
            lmcCriticalBasis = mrdCriticalBasis reducedData,
            lmcProjection = mrdProjection reducedData,
            lmcInclusion = mrdInclusion reducedData,
            lmcHomotopy = mrdHomotopy reducedData
          }
  where
    allCells = allBasisCellRefs chainComplex
    initialEdgeMap = boundaryEdgeMap chainComplex
    reducedMatchingPairs = lamPairs matching
    criticalCells = expectedCriticalCells localizedLowerCell localizedUpperCell allCells reducedMatchingPairs
    finalEdgeMap = foldl' (flip (reverseCandidateEdgeWith rationalMorsePivotOps)) initialEdgeMap reducedMatchingPairs

buildMatchingBy ::
  [BasisCellRef] ->
  (BasisCellRef -> Double) ->
  DirectedEdgeMap edgeWeight ->
  [pair] ->
  (pair -> BasisCellRef) ->
  (pair -> BasisCellRef) ->
  (pair -> DirectedEdgeMap edgeWeight -> DirectedEdgeMap edgeWeight) ->
  (pair -> [BasisCellRef] -> obstruction) ->
  ([pair] -> [BasisCellRef] -> [obstruction] -> matching) ->
  matching
buildMatchingBy allCells cellScore initialEdgeMap initialCandidates lowerCell upperCell reverseEdge makeObstruction makeMatching =
  go initialGradientState Set.empty [] [] orderedCandidates
  where
    initialGradientState = gradientAcyclicState initialEdgeMap
    orderedCandidates = sortCandidates lowerCell upperCell cellScore initialCandidates
    go currentGradientState matchedCells acceptedPairs acceptedObstructions remainingCandidates =
      case remainingCandidates of
        [] ->
          let finalizedPairs = reverse acceptedPairs
           in makeMatching
                finalizedPairs
                (expectedCriticalCells lowerCell upperCell allCells finalizedPairs)
                (reverse acceptedObstructions)
        candidatePair : restCandidates
          | Set.member lower matchedCells || Set.member upper matchedCells ->
              go currentGradientState matchedCells acceptedPairs acceptedObstructions restCandidates
          | otherwise ->
              case reverseAcyclicState (reverseEdge candidatePair) upper lower currentGradientState of
                AcyclicRewriteRejected witnessPath ->
                  go
                    currentGradientState
                    matchedCells
                    acceptedPairs
                    (makeObstruction candidatePair (lower : witnessPath) : acceptedObstructions)
                    restCandidates
                AcyclicRewriteAccepted nextGradientState ->
                  go
                    nextGradientState
                    (Set.insert lower (Set.insert upper matchedCells))
                    (candidatePair : acceptedPairs)
                    acceptedObstructions
                    restCandidates
          where
            lower = lowerCell candidatePair
            upper = upperCell candidatePair

type MorseReductionData :: Type -> Type
data MorseReductionData r = MorseReductionData
  { mrdReducedComplex :: !(FiniteChainComplex r),
    mrdCriticalBasis :: !(Map BasisCellRef BasisCellRef),
    mrdProjection :: !(ChainMap BasisCellRef BasisCellRef r),
    mrdInclusion :: !(ChainMap BasisCellRef BasisCellRef r),
    mrdHomotopy :: !(ChainHomotopy BasisCellRef r)
  }

type BoundaryLookup :: Type -> Type
newtype BoundaryLookup r = BoundaryLookup
  (Map BasisCellRef [(r, BasisCellRef)])

buildReducedComplexData ::
  (Eq r, Num r, Semiring r) =>
  FiniteChainComplex r ->
  [BasisCellRef] ->
  [AlgebraicMorsePair r] ->
  DirectedEdgeMap r ->
  DirectedEdgeMap r ->
  Either HomologyFailure (MorseReductionData r)
buildReducedComplexData chainComplex criticalCells matchingPairs originalEdgeMap edgeMap = do
  validateMorseReductionInput chainComplex originalEdgeMap
  buildReducedComplexDataTrusted chainComplex criticalCells matchingPairs edgeMap

-- | The reduction body without the input gate. Interior callers (the refined
-- descent, stages ≥ 2) operate on complexes that are Morse reductions of an
-- already-validated complex — chain complexes by the reduction theorem — so
-- re-running shape and nilpotence validation there would re-prove a theorem
-- per stage. Exterior entry points go through 'buildReducedComplexData'.
buildReducedComplexDataTrusted ::
  (Eq r, Num r, Semiring r) =>
  FiniteChainComplex r ->
  [BasisCellRef] ->
  [AlgebraicMorsePair r] ->
  DirectedEdgeMap r ->
  Either HomologyFailure (MorseReductionData r)
buildReducedComplexDataTrusted chainComplex criticalCells matchingPairs edgeMap = do
  let originalCells = allBasisCellRefs chainComplex
      complexMaxDegree = maxHomologicalDegree chainComplex
      HomologicalDegree complexMaxDimension = complexMaxDegree
      adjacency = adjacencyMap edgeMap
      topologicalVertexOrder = topologicalOrderWithVertices originalCells adjacency
      sortedCriticalCells = sortOn basisCellKey criticalCells
      criticalCellsByDegree =
        Map.fromListWith (<>)
          [(basisCellDimension cell, [cell]) | cell <- sortedCriticalCells]
      criticalCellsAt degreeValue = Map.findWithDefault [] degreeValue criticalCellsByDegree
      pathWeightsBySource =
        pathWeightOracle edgeMap topologicalVertexOrder
      criticalBasis =
        Map.fromList
          [ ( BasisCellRef
                { cellDegree = HomologicalDegree degreeValue,
                  cellIndex = reducedIndex
                },
              originalCell
            )
          | degreeValue <- [0 .. complexMaxDimension],
            (reducedIndex, originalCell) <- zip [0 ..] (criticalCellsAt degreeValue)
          ]
      criticalOriginalToReduced =
        Map.fromList [(originalCell, reducedCell) | (reducedCell, originalCell) <- Map.toAscList criticalBasis]
      (projectionMap, inclusionMap, homotopyMap) =
        morseReductionMaps
          pathWeightsBySource
          criticalBasis
          criticalOriginalToReduced
          matchingPairs
  incidencesByDegree <-
    traverse
      ( \degreeValue ->
          (\incidence -> (degreeValue, incidence))
            <$> materializeIncidenceBoundary
              (reducedBoundaryOf pathWeightsBySource (criticalCellsAt (degreeValue - 1)))
              (criticalCellsAt degreeValue)
              (criticalCellsAt (degreeValue - 1))
      )
      [0 .. complexMaxDimension]
  let incidenceByDegree = Map.fromList incidencesByDegree
  let reducedComplex =
        mkFiniteChainComplex
          complexMaxDegree
          (\(HomologicalDegree degreeValue) -> Map.findWithDefault emptyBoundaryIncidence degreeValue incidenceByDegree)
  pure
    MorseReductionData
      { mrdReducedComplex = reducedComplex,
        mrdCriticalBasis = criticalBasis,
        mrdProjection = projectionMap,
        mrdInclusion = inclusionMap,
        mrdHomotopy = homotopyMap
      }

validateMorseReductionInput ::
  (Eq r, Num r) =>
  FiniteChainComplex r ->
  DirectedEdgeMap r ->
  Either HomologyFailure ()
validateMorseReductionInput chainComplex originalEdgeMap = do
  validateFiniteChainComplexShape chainComplex
  validateBoundaryNilpotenceAsMorseObstruction (boundaryLookupFromEdgeMap originalEdgeMap) (allBasisCellRefs chainComplex)

validateBoundaryNilpotenceAsMorseObstruction ::
  (Eq r, Num r) =>
  BoundaryLookup r ->
  [BasisCellRef] ->
  Either HomologyFailure ()
validateBoundaryNilpotenceAsMorseObstruction boundaryLookupValue cells =
  -- The condition checked here is d ∘ d = 0 on the input complex; report it
  -- as the nilpotence law (as every other detection path does), not as a
  -- reduction chain-map violation.
  LC.checkLawWith
    LC.numArithmetic
    ChainNilpotenceLaw
    cells
    (const [])
    (LC.composeWith LC.numArithmetic (morseBoundaryOfLookup boundaryLookupValue) . morseBoundaryOfLookup boundaryLookupValue)

reducedBoundaryOf :: (Eq r, Num r) => PathWeightOracle r -> [BasisCellRef] -> BasisCellRef -> [(r, BasisCellRef)]
reducedBoundaryOf pathWeightSums targetCells upperCell =
  -- One O(V+E) path-weight propagation per upper cell, shared by every
  -- lower cell — floated out of the lambda so the sharing does not depend
  -- on the optimizer's full-laziness pass.
  let weightsFromUpper = pathWeightsFromOracle pathWeightSums upperCell
   in targetCells
        & mapMaybe
          ( \lowerCell ->
              case Map.lookup lowerCell weightsFromUpper of
                Just coefficientValue
                  | coefficientValue /= 0 -> Just (coefficientValue, lowerCell)
                _ -> Nothing
          )

morseReductionMaps ::
  (Eq r, Num r) =>
  PathWeightOracle r ->
  Map BasisCellRef BasisCellRef ->
  Map BasisCellRef BasisCellRef ->
  [AlgebraicMorsePair r] ->
  (ChainMap BasisCellRef BasisCellRef r, ChainMap BasisCellRef BasisCellRef r, ChainHomotopy BasisCellRef r)
morseReductionMaps pathWeightSums criticalBasis criticalOriginalToReduced matchingPairs =
  (projectionMap, inclusionMap, homotopyMap)
  where
    pathWeightsFrom =
      pathWeightsFromOracle pathWeightSums

    matchedUpperCells =
      Set.fromList (fmap localizedUpperCell matchingPairs)

    projectionMap =
      ChainMap $
        \originalCell ->
          LC.normalizeWith LC.numArithmetic
            [ (coefficientValue, reducedCell)
            | (targetCell, coefficientValue) <- Map.toAscList (pathWeightsFrom originalCell),
              coefficientValue /= 0,
              cellDegree targetCell == cellDegree originalCell,
              Just reducedCell <- [Map.lookup targetCell criticalOriginalToReduced]
            ]

    inclusionMap =
      ChainMap $
        \reducedCell ->
          case Map.lookup reducedCell criticalBasis of
            Nothing -> []
            Just criticalOriginal ->
              LC.normalizeWith LC.numArithmetic
                [ (coefficientValue, targetCell)
                | (targetCell, coefficientValue) <- Map.toAscList (pathWeightsFrom criticalOriginal),
                  coefficientValue /= 0,
                  cellDegree targetCell == cellDegree criticalOriginal
                ]

    homotopyMap =
      ChainHomotopy $
        \originalCell ->
          LC.normalizeWith LC.numArithmetic
            [ (negate coefficientValue, targetCell)
            | (targetCell, coefficientValue) <- Map.toAscList (pathWeightsFrom originalCell),
              coefficientValue /= 0,
              Set.member targetCell matchedUpperCells,
              basisCellDimension targetCell == basisCellDimension originalCell + 1
            ]

morseBoundaryOfLookup ::
  BoundaryLookup r ->
  BasisCellRef ->
  [(r, BasisCellRef)]
morseBoundaryOfLookup (BoundaryLookup boundaryBySource) cell =
  Map.findWithDefault [] cell boundaryBySource

boundaryLookupFromEdgeMap :: (Eq r, Num r) => DirectedEdgeMap r -> BoundaryLookup r
boundaryLookupFromEdgeMap edgeMap =
  BoundaryLookup
    (Map.map (LC.normalizeWith LC.numArithmetic) boundaryBySource)
  where
    indexedBoundaryEntries =
      [ (sourceCell, targetCell, coefficientValue)
      | ((sourceCell, targetCell), coefficientValue) <- Map.toAscList edgeMap,
        coefficientValue /= 0
      ]
    boundaryBySource =
      Map.fromListWith
        (<>)
        [ (sourceCell, [(coefficientValue, targetCell)])
        | (sourceCell, targetCell, coefficientValue) <- indexedBoundaryEntries
        ]

integralMatchingFromAlgebraic :: AlgebraicMorseMatching Integer -> AcyclicMatching
integralMatchingFromAlgebraic matching =
  AcyclicMatching
    { amPairs = fmap integralPairFromAlgebraic (lamPairs matching),
      amCriticalCells = lamCriticalCells matching,
      amObstructions = fmap integralObstructionFromAlgebraic (lamObstructions matching)
    }

integralPairFromAlgebraic :: AlgebraicMorsePair Integer -> AcyclicPair
integralPairFromAlgebraic candidatePair =
  AcyclicPair
    { apLowerCell = localizedLowerCell candidatePair,
      apUpperCell = localizedUpperCell candidatePair,
      apIncidenceCoefficient = fromIntegral (localizedIncidenceCoefficient candidatePair)
    }

integralObstructionFromAlgebraic ::
  LocalizedCollapseObstruction (AlgebraicMorsePair Integer) ->
  CollapseObstruction
integralObstructionFromAlgebraic obstruction =
  CollapseObstruction
    { coCandidate = integralPairFromAlgebraic (lcoCandidate obstruction),
      coCycleWitness = lcoCycleWitness obstruction
    }

integralMatchingToAlgebraic :: Num r => AcyclicMatching -> AlgebraicMorseMatching r
integralMatchingToAlgebraic matching =
  LocalizedAcyclicMatching
    { lamPairs = fmap integralPairToAlgebraic (amPairs matching),
      lamCriticalCells = amCriticalCells matching,
      lamObstructions = fmap integralObstructionToAlgebraic (amObstructions matching)
    }

integralPairToAlgebraic :: Num r => AcyclicPair -> AlgebraicMorsePair r
integralPairToAlgebraic candidatePair =
  LocalizedAcyclicPair
    { lapLowerCell = apLowerCell candidatePair,
      lapUpperCell = apUpperCell candidatePair,
      lapIncidenceCoefficient = fromIntegral (apIncidenceCoefficient candidatePair)
    }

integralObstructionToAlgebraic ::
  Num r =>
  CollapseObstruction ->
  LocalizedCollapseObstruction (AlgebraicMorsePair r)
integralObstructionToAlgebraic obstruction =
  LocalizedCollapseObstruction
    { lcoCandidate = integralPairToAlgebraic (coCandidate obstruction),
      lcoCycleWitness = coCycleWitness obstruction
    }

boundaryEdgeMap :: (Eq r, Num r) => FiniteChainComplex r -> DirectedEdgeMap r
boundaryEdgeMap chainComplex =
  Map.filter (/= 0) $
    Map.fromListWith (+)
      [ (edgeKey, coefficientValue)
      | degreeValue <- [1 .. maxDegreeValue],
        let boundaryIncidence = incidenceMatrixAt chainComplex (HomologicalDegree degreeValue),
        boundaryEntry <- boundaryEntries boundaryIncidence,
        let coefficientValue = boundaryCoefficient boundaryEntry,
        let edgeKey =
              ( BasisCellRef
                  { cellDegree = HomologicalDegree degreeValue,
                    cellIndex = sourceIndex boundaryEntry
                  },
                BasisCellRef
                  { cellDegree = HomologicalDegree (degreeValue - 1),
                    cellIndex = targetIndex boundaryEntry
                  }
              )
      ]
  where
    HomologicalDegree maxDegreeValue = maxHomologicalDegree chainComplex

integralBoundaryEdgeMap :: Integral r => FiniteChainComplex r -> DirectedEdgeMap Integer
integralBoundaryEdgeMap = Map.map toInteger . boundaryEdgeMap

rationalizeFiniteChainComplex :: Integral r => FiniteChainComplex r -> FiniteChainComplex Rational
rationalizeFiniteChainComplex chainComplex =
  mkFiniteChainComplex
    (maxHomologicalDegree chainComplex)
    (mapBoundaryCoefficients fromIntegral . incidenceMatrixAt chainComplex)

expectedCriticalCells ::
  (pair -> BasisCellRef) ->
  (pair -> BasisCellRef) ->
  [BasisCellRef] ->
  [pair] ->
  [BasisCellRef]
expectedCriticalCells lowerCell upperCell allCells matchingPairs =
  filter (`Set.notMember` matchedCells) allCells
  where
    matchedCells =
      Set.fromList
        [ cell
        | candidatePair <- matchingPairs,
          cell <- [lowerCell candidatePair, upperCell candidatePair]
        ]

uniquePairEndpoints ::
  (pair -> BasisCellRef) ->
  (pair -> BasisCellRef) ->
  [pair] ->
  Bool
uniquePairEndpoints lowerCell upperCell matchingPairs =
  length endpointCells == Set.size (Set.fromList endpointCells)
  where
    endpointCells =
      [ cell
      | candidatePair <- matchingPairs,
        cell <- [lowerCell candidatePair, upperCell candidatePair]
      ]

sortCandidates ::
  (pair -> BasisCellRef) ->
  (pair -> BasisCellRef) ->
  (BasisCellRef -> Double) ->
  [pair] ->
  [pair]
sortCandidates lowerCell upperCell cellScore =
  sortOn
    (\candidatePair ->
       ( cellScore (upperCell candidatePair),
         cellScore (lowerCell candidatePair),
         basisCellKey (upperCell candidatePair),
         basisCellKey (lowerCell candidatePair)
       )
    )

gradientAcyclicState ::
  DirectedEdgeMap edgeWeight ->
  GradientAcyclicState edgeWeight
gradientAcyclicState edgeMap =
  GradientAcyclicState
    { gasEdgeMap = edgeMap,
      gasAdjacency = adjacencyMap edgeMap,
      gasMatchedUpperByLower = Map.empty,
      gasGradientAdjacency = Map.empty
    }

reverseAcyclicState ::
  (DirectedEdgeMap edgeWeight -> DirectedEdgeMap edgeWeight) ->
  BasisCellRef ->
  BasisCellRef ->
  GradientAcyclicState edgeWeight ->
  AcyclicRewrite edgeWeight
reverseAcyclicState reverseEdge upperCell lowerCell state =
  let lowerTargets =
        gasAdjacency state
          & Map.findWithDefault [] upperCell
          & filter (/= lowerCell)
   in case gradientCycleWitness upperCell lowerCell lowerTargets state of
        Just witnessPath ->
          AcyclicRewriteRejected witnessPath
        Nothing ->
          AcyclicRewriteAccepted
            GradientAcyclicState
              { gasEdgeMap = reverseEdge (gasEdgeMap state),
                gasAdjacency = insertAdjacencyEdge lowerCell upperCell (deleteAdjacencyEdge upperCell lowerCell (gasAdjacency state)),
                gasMatchedUpperByLower = Map.insert lowerCell upperCell (gasMatchedUpperByLower state),
                gasGradientAdjacency =
                  if null lowerTargets
                    then gasGradientAdjacency state
                    else Map.insert lowerCell lowerTargets (gasGradientAdjacency state)
              }

gradientCycleWitness ::
  BasisCellRef ->
  BasisCellRef ->
  [BasisCellRef] ->
  GradientAcyclicState edgeWeight ->
  Maybe [BasisCellRef]
gradientCycleWitness upperCell lowerCell lowerTargets state =
  firstJust
    ( \targetCell ->
        gradientPathInAdjacency (gasGradientAdjacency state) targetCell lowerCell
          >>= directedPathFromGradientPath upperCell (gasMatchedUpperByLower state)
    )
    lowerTargets

gradientPathInAdjacency ::
  DirectedAdjacencyMap ->
  BasisCellRef ->
  BasisCellRef ->
  Maybe [BasisCellRef]
gradientPathInAdjacency adjacency startCell goalCell =
  snd (findFrom Set.empty startCell)
  where
    findFrom visited currentCell
      | currentCell == goalCell = (visited, Just [goalCell])
      | Set.member currentCell visited = (visited, Nothing)
      | otherwise =
          foldl'
            (findThroughSuccessor currentCell)
            (Set.insert currentCell visited, Nothing)
            (Map.findWithDefault [] currentCell adjacency)
    findThroughSuccessor currentCell (visited, discoveredPath) successorCell =
      case discoveredPath of
        Just pathValue -> (visited, Just pathValue)
        Nothing ->
          case findFrom visited successorCell of
            (visitedAfterSuccessor, Just suffixPath) ->
              (visitedAfterSuccessor, Just (currentCell : suffixPath))
            (visitedAfterSuccessor, Nothing) ->
              (visitedAfterSuccessor, Nothing)

directedPathFromGradientPath ::
  BasisCellRef ->
  Map BasisCellRef BasisCellRef ->
  [BasisCellRef] ->
  Maybe [BasisCellRef]
directedPathFromGradientPath upperCell matchedUpperByLower gradientPath =
  fmap (upperCell :) (expandGradientPath gradientPath)
  where
    expandGradientPath pathValue =
      case pathValue of
        [] -> Just []
        [terminalLower] -> Just [terminalLower]
        lowerValue : remainingPath@(_ : _) ->
          case Map.lookup lowerValue matchedUpperByLower of
            Nothing -> Nothing
            Just matchedUpper ->
              fmap ((lowerValue :) . (matchedUpper :)) (expandGradientPath remainingPath)

firstJust :: (a -> Maybe b) -> [a] -> Maybe b
firstJust selectValue =
  foldr
    ( \value restValue ->
        case selectValue value of
          Just selectedValue -> Just selectedValue
          Nothing -> restValue
    )
    Nothing

rebaseCell :: Map BasisCellRef BasisCellRef -> BasisCellRef -> BasisCellRef
rebaseCell basis cell = Map.findWithDefault cell cell basis

rebaseLocalizedPair ::
  Map BasisCellRef BasisCellRef ->
  LocalizedAcyclicPair r ->
  LocalizedAcyclicPair r
rebaseLocalizedPair basis candidatePair =
  LocalizedAcyclicPair
    { lapLowerCell = rebaseCell basis (localizedLowerCell candidatePair),
      lapUpperCell = rebaseCell basis (localizedUpperCell candidatePair),
      lapIncidenceCoefficient = localizedIncidenceCoefficient candidatePair
    }

rebaseLocalizedMatching ::
  Map BasisCellRef BasisCellRef ->
  LocalizedAcyclicMatching RationalAcyclicPair ->
  LocalizedAcyclicMatching RationalAcyclicPair
rebaseLocalizedMatching basis matching =
  LocalizedAcyclicMatching
    { lamPairs = fmap (rebaseLocalizedPair basis) (lamPairs matching),
      lamCriticalCells = fmap (rebaseCell basis) (lamCriticalCells matching),
      lamObstructions = fmap (rebaseLocalizedObstruction basis) (lamObstructions matching)
    }

rebaseLocalizedObstruction ::
  Map BasisCellRef BasisCellRef ->
  LocalizedCollapseObstruction RationalAcyclicPair ->
  LocalizedCollapseObstruction RationalAcyclicPair
rebaseLocalizedObstruction basis obstruction =
  LocalizedCollapseObstruction
    { lcoCandidate = rebaseLocalizedPair basis (lcoCandidate obstruction),
      lcoCycleWitness = fmap (rebaseCell basis) (lcoCycleWitness obstruction)
    }

composeCriticalBasis ::
  Map BasisCellRef BasisCellRef ->
  Map BasisCellRef BasisCellRef ->
  Map BasisCellRef BasisCellRef
composeCriticalBasis priorBasis = fmap (rebaseCell priorBasis)

localizedLowerCell :: LocalizedAcyclicPair r -> BasisCellRef
localizedLowerCell LocalizedAcyclicPair {lapLowerCell = lowerCell} = lowerCell

localizedUpperCell :: LocalizedAcyclicPair r -> BasisCellRef
localizedUpperCell LocalizedAcyclicPair {lapUpperCell = upperCell} = upperCell

localizedIncidenceCoefficient :: LocalizedAcyclicPair r -> r
localizedIncidenceCoefficient LocalizedAcyclicPair {lapIncidenceCoefficient = coefficientValue} = coefficientValue

isCodimensionOne :: BasisCellRef -> BasisCellRef -> Bool
isCodimensionOne lowerCell upperCell =
  basisCellDimension upperCell == basisCellDimension lowerCell + 1