packages feed

moonlight-homology-0.1.0.0: src-matrix/Moonlight/Homology/Pure/Rank/Field.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE StandaloneKindSignatures #-}

module Moonlight.Homology.Pure.Rank.Field
  ( FieldRankBackend (..),
    fieldBettiCapability,
  )
where

import Data.Bifunctor (first)
import Data.Foldable (traverse_)
import Data.IntMap.Strict (IntMap)
import Data.IntMap.Strict qualified as IntMap
import Data.Kind (Type)
import Data.Maybe (listToMaybe)
import Moonlight.Core (Semiring, mkCapability)
import Moonlight.Homology.Boundary.Finite
  ( FiniteChainComplex,
    degreeCardinality,
    incidenceMatrixAt,
    maxHomologicalDegree,
    validateFiniteChainComplexShape,
  )
import Moonlight.Homology.Boundary.LinAlg
  ( BoundaryIncidence,
    BoundaryIncidenceShapeError,
    boundaryEntries,
    composeBoundaryIncidence,
  )
import Moonlight.Homology.Pure.Matrix.Reducer
  ( BettiCapability,
    BettiReducer (..),
  )
import Moonlight.Homology.Pure.Degree
  ( HomologicalDegree (..),
  )
import Moonlight.Homology.Pure.Failure
  ( HomologyFailure (..),
  )
import Moonlight.Homology.Pure.Group
  ( HomologyGroup (..),
  )
import Moonlight.Homology.Pure.Phase
  ( RequirePhase2,
  )
import Moonlight.Homology.Pure.Rank.GF2
  ( GF2RankFailure,
    gf2BoundaryRank,
  )
import Moonlight.Homology.Pure.Matrix.SparseLinAlg
  ( SparseRref (..),
    sparseBoundaryMatrixWith,
    sparseRref,
  )
import Moonlight.LinAlg.Pure.Dense.GF2
  ( GF2,
  )

type FieldRankBackend :: Type -> Type
data FieldRankBackend coeff where
  RationalFieldRankBackend :: FieldRankBackend Rational
  GF2FieldRankBackend :: FieldRankBackend GF2

deriving stock instance Eq (FieldRankBackend coeff)

deriving stock instance Show (FieldRankBackend coeff)

type FieldRankFailure :: Type
data FieldRankFailure
  = FieldRankGF2Failed !GF2RankFailure
  deriving stock (Eq, Show)

type FieldHomologyFailure :: Type -> Type
data FieldHomologyFailure coeff
  = FieldHomologyInvalidChainShape !HomologyFailure
  | FieldHomologyBoundaryShapeFailed !HomologicalDegree !BoundaryIncidenceShapeError
  | FieldHomologyNonNilpotent !HomologicalDegree !(BoundaryIncidence coeff)
  | FieldHomologyRankFailed !HomologicalDegree !FieldRankFailure
  | FieldHomologyRankMissing !HomologicalDegree
  | FieldHomologyNegativeDimension !HomologicalDegree !Int !Int !Int
  deriving stock (Eq, Show)

fieldBettiCapability ::
  RequirePhase2 phase =>
  FieldRankBackend coeff ->
  BettiCapability phase coeff
fieldBettiCapability backend =
  mkCapability (fieldBettiReducer backend)
{-# INLINEABLE fieldBettiCapability #-}

fieldBettiReducer ::
  FieldRankBackend coeff ->
  BettiReducer coeff
fieldBettiReducer backend =
  BettiReducer $
    first fieldHomologyFailureToHomologyFailure
      . fieldBettiGroups backend
{-# INLINEABLE fieldBettiReducer #-}

fieldBettiGroups ::
  FieldRankBackend coeff ->
  FiniteChainComplex coeff ->
  Either (FieldHomologyFailure coeff) [HomologyGroup coeff]
fieldBettiGroups backend finite =
  IntMap.elems <$> fieldBettiGroupsByDegree backend finite
{-# INLINEABLE fieldBettiGroups #-}

fieldBettiGroupsByDegree ::
  FieldRankBackend coeff ->
  FiniteChainComplex coeff ->
  Either (FieldHomologyFailure coeff) (IntMap (HomologyGroup coeff))
fieldBettiGroupsByDegree backend finite =
  case backend of
    RationalFieldRankBackend ->
      fieldBettiGroupsWith rationalRank finite
    GF2FieldRankBackend ->
      fieldBettiGroupsWith rankGF2Boundary finite
{-# INLINEABLE fieldBettiGroupsByDegree #-}

fieldBettiGroupsWith ::
  (Eq coeff, Num coeff, Semiring coeff) =>
  (BoundaryIncidence coeff -> Either FieldRankFailure Int) ->
  FiniteChainComplex coeff ->
  Either (FieldHomologyFailure coeff) (IntMap (HomologyGroup coeff))
fieldBettiGroupsWith rankBoundary finite = do
  first FieldHomologyInvalidChainShape $
    validateFiniteChainComplexShape finite
  traverse_ (validateNilpotenceAt finite) (positiveDegrees finite)
  rankByDegree <- rankBoundariesByDegree rankBoundary finite
  IntMap.fromAscList
    <$> traverse (fieldHomologyAt rankByDegree) (degrees finite)
  where
    fieldHomologyAt rankByDegree degreeValue@(HomologicalDegree degreeInt) = do
      let chainDimension =
            degreeCardinality finite degreeValue
      boundaryRank <-
        rankAt rankByDegree degreeValue
      nextBoundaryRank <-
        rankAt rankByDegree (HomologicalDegree (degreeInt + 1))
      let homologyDimension =
            chainDimension - boundaryRank - nextBoundaryRank
      if homologyDimension < 0
        then
          Left
            ( FieldHomologyNegativeDimension
                degreeValue
                chainDimension
                boundaryRank
                nextBoundaryRank
            )
        else
          Right
            ( degreeInt,
              HomologyGroup
                { freeRank = homologyDimension,
                  torsionInvariants = []
                }
            )

-- | Ranks of every boundary map the homology formula consults. Degrees above
-- 'maxHomologicalDegree' are zero by the complex's own convention
-- ('degreeCardinality' clamps them to cardinality 0), so the rank at
-- @max + 1@ is 0 by definition — it is deliberately NOT read from the raw
-- incidence function, which is unvalidated beyond the top degree and would
-- otherwise silently corrupt the top homology group.
rankBoundariesByDegree ::
  (BoundaryIncidence coeff -> Either FieldRankFailure Int) ->
  FiniteChainComplex coeff ->
  Either (FieldHomologyFailure coeff) (IntMap Int)
rankBoundariesByDegree rankBoundary finite =
  IntMap.insert (maxDegreeInt + 1) 0 . IntMap.fromAscList
    <$> traverse rankBoundaryAt (degrees finite)
  where
    HomologicalDegree maxDegreeInt = maxHomologicalDegree finite
    rankBoundaryAt degreeValue@(HomologicalDegree degreeInt) =
      fmap
        (\rankValue -> (degreeInt, rankValue))
        ( first (FieldHomologyRankFailed degreeValue) $
            rankBoundary (incidenceMatrixAt finite degreeValue)
        )
{-# INLINEABLE rankBoundariesByDegree #-}

rankAt ::
  IntMap Int ->
  HomologicalDegree ->
  Either (FieldHomologyFailure coeff) Int
rankAt rankByDegree degreeValue@(HomologicalDegree degreeInt) =
  maybe
    (Left (FieldHomologyRankMissing degreeValue))
    Right
    (IntMap.lookup degreeInt rankByDegree)
{-# INLINE rankAt #-}

degrees ::
  FiniteChainComplex coeff ->
  [HomologicalDegree]
degrees finite =
  case maxHomologicalDegree finite of
    HomologicalDegree maxDegreeValue ->
      fmap HomologicalDegree [0 .. maxDegreeValue]
{-# INLINE degrees #-}

positiveDegrees ::
  FiniteChainComplex coeff ->
  [HomologicalDegree]
positiveDegrees finite =
  case maxHomologicalDegree finite of
    HomologicalDegree maxDegreeValue ->
      fmap HomologicalDegree [1 .. maxDegreeValue]
{-# INLINE positiveDegrees #-}

validateNilpotenceAt ::
  (Eq coeff, Num coeff, Semiring coeff) =>
  FiniteChainComplex coeff ->
  HomologicalDegree ->
  Either (FieldHomologyFailure coeff) ()
validateNilpotenceAt finite degreeValue@(HomologicalDegree degreeInt) =
  case
    composeBoundaryIncidence
      (incidenceMatrixAt finite (HomologicalDegree (degreeInt - 1)))
      (incidenceMatrixAt finite degreeValue)
  of
    Left shapeError ->
      Left (FieldHomologyBoundaryShapeFailed degreeValue shapeError)
    Right composite ->
      case listToMaybe (boundaryEntries composite) of
        Nothing ->
          Right ()
        Just _ ->
          Left (FieldHomologyNonNilpotent degreeValue composite)
{-# INLINEABLE validateNilpotenceAt #-}

rationalRank ::
  BoundaryIncidence Rational ->
  Either FieldRankFailure Int
rationalRank =
  Right
    . length
    . srrefPivots
    . sparseRref
    . sparseBoundaryMatrixWith id
{-# INLINE rationalRank #-}

rankGF2Boundary ::
  BoundaryIncidence GF2 ->
  Either FieldRankFailure Int
rankGF2Boundary =
  first FieldRankGF2Failed . gf2BoundaryRank
{-# INLINEABLE rankGF2Boundary #-}

fieldHomologyFailureToHomologyFailure ::
  FieldHomologyFailure coeff ->
  HomologyFailure
fieldHomologyFailureToHomologyFailure failureValue =
  case failureValue of
    FieldHomologyInvalidChainShape failure ->
      failure
    FieldHomologyBoundaryShapeFailed degreeValue shapeError ->
      InvalidBoundaryIncidence
        ("field homology boundary composition failed at " <> show degreeValue <> ": " <> show shapeError)
    FieldHomologyNonNilpotent (HomologicalDegree degreeInt) _composite ->
      -- 'validateNilpotenceAt' composes ∂ at (degree - 1, degree); the
      -- convention (matching 'adjacentNilpotenceAt' in Boundary.Finite)
      -- names the lower degree of the composed pair, so the same failure
      -- surfaces as the same constructor on every detection path.
      ChainComplexNilpotenceViolation (degreeInt - 1)
    FieldHomologyRankFailed degreeValue rankFailure ->
      BackendFailure
        ("field homology rank failed at " <> show degreeValue <> ": " <> show rankFailure)
    FieldHomologyRankMissing degreeValue ->
      BackendFailure
        ("field homology rank cache missed degree " <> show degreeValue)
    FieldHomologyNegativeDimension degreeValue chainDimension boundaryRank nextBoundaryRank ->
      InvalidTopologyInput
        ( "field homology dimension went negative at "
            <> show degreeValue
            <> ": chain="
            <> show chainDimension
            <> ", boundaryRank="
            <> show boundaryRank
            <> ", nextBoundaryRank="
            <> show nextBoundaryRank
        )
{-# INLINE fieldHomologyFailureToHomologyFailure #-}