packages feed

moonlight-homology-0.1.0.0: test/matrix/FieldBettiSpec.hs

{-# LANGUAGE DataKinds #-}

module FieldBettiSpec
  ( tests,
  )
where

import Moonlight.Homology
import Moonlight.Homology.Boundary.Finite (mkFiniteChainComplex)
import TestFixtures
  ( mooreComplex,
  )
import Moonlight.LinAlg.Pure.Dense.GF2 (GF2)
import Numeric.Natural (Natural)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit
  ( Assertion,
    assertEqual,
    assertFailure,
    testCase,
  )

tests :: TestTree
tests =
  testGroup
    "homology backend substrate"
    [ testCase "GF2 Betti uses linalg rank for zero and identity matrices" testGF2ZeroAndIdentityBetti,
      testCase "GF2 Betti handles duplicate rows and duplicate-entry cancellation" testGF2DuplicateRowsAndEntryCancellation,
      testCase "GF2 coefficient mapping uses parity for signed integral boundaries" testGF2ParityConversion,
      testCase "typed homology backend dispatches Smith, Rational, and GF2 ranks" testHomologyBackendDispatch,
      testCase "field Betti computes isolated points and an interval through Phase 2" testFieldBettiBasicComplexes,
      testCase "field Betti rejects malformed and non-nilpotent chains through Phase 2" testFieldBettiRejectsInvalidChains,
      testCase "checked constructor refuses shape and nilpotence violations at the seal" testCheckedConstructorSeal
    ]

testGF2ZeroAndIdentityBetti :: Assertion
testGF2ZeroAndIdentityBetti = do
  zeroComplex <- expectRight (gf2OneBoundaryComplex 3 2 [])
  zeroGroups <- gf2Betti zeroComplex
  assertEqual "zero differential H0" [2, 3] (fmap freeRank zeroGroups)
  identityComplex <-
    expectRight $
      gf2OneBoundaryComplex
        3
        3
        [ mkBoundaryEntry 0 0 1,
          mkBoundaryEntry 1 1 1,
          mkBoundaryEntry 2 2 1
        ]
  identityGroups <- gf2Betti identityComplex
  assertEqual "identity differential is acyclic" [0, 0] (fmap freeRank identityGroups)

testGF2DuplicateRowsAndEntryCancellation :: Assertion
testGF2DuplicateRowsAndEntryCancellation = do
  duplicateRowComplex <-
    expectRight $
      gf2OneBoundaryComplex
        1
        2
        [ mkBoundaryEntry 0 0 1,
          mkBoundaryEntry 0 1 1
        ]
  duplicateRowGroups <- gf2Betti duplicateRowComplex
  assertEqual "duplicate row rank contributes once" [1, 0] (fmap freeRank duplicateRowGroups)
  cancelledComplex <-
    expectRight $
      gf2OneBoundaryComplex
        1
        1
        [ mkBoundaryEntry 0 0 1,
          mkBoundaryEntry 0 0 1
        ]
  cancelledGroups <- gf2Betti cancelledComplex
  assertEqual "duplicate GF2 entries cancel" [1, 1] (fmap freeRank cancelledGroups)

testGF2ParityConversion :: Assertion
testGF2ParityConversion = do
  integralBoundary <-
    expectRight $
      mkBoundaryIncidence
        4
        1
        [ mkBoundaryEntry 0 0 (2 :: Int),
          mkBoundaryEntry 1 0 (-2 :: Int),
          mkBoundaryEntry 2 0 (1 :: Int),
          mkBoundaryEntry 3 0 (-1 :: Int)
        ]
  let parityBoundary =
        mapBoundaryCoefficients fromIntegral integralBoundary
      parityComplex =
        mkFiniteChainComplex (HomologicalDegree 1) $ \degreeValue ->
          case degreeValue of
            HomologicalDegree 1 ->
              parityBoundary
            HomologicalDegree 0 ->
              emptyBoundaryIncidenceOf 1 0
            _ ->
              emptyBoundaryIncidence
  parityGroups <- gf2Betti parityComplex
  assertEqual "even entries vanish and odd signs survive" [0, 3] (fmap freeRank parityGroups)

testHomologyBackendDispatch :: Assertion
testHomologyBackendDispatch = do
  integralGroups <-
    expectRight
      (runHomologyBackend (IntegralSmithBackend :: HomologyBackend Integer Integer) mooreComplex)
  assertEqual "Smith backend preserves Moore torsion" [[], [2], []] (fmap torsionInvariants integralGroups)
  assertEqual "Smith backend tag" IntegralSmithBackendTag (homologyBackendTag (IntegralSmithBackend :: HomologyBackend Integer Integer))
  rationalComplex <- expectRight rationalIntervalComplex
  rationalGroups <- expectRight (runHomologyBackend RationalRankBackend rationalComplex)
  assertEqual "Rational backend interval Betti" [1, 0] (fmap freeRank rationalGroups)
  assertEqual "Rational backend tag" RationalRankBackendTag (homologyBackendTag RationalRankBackend)
  gf2Complex <- expectRight (gf2OneBoundaryComplex 1 1 [mkBoundaryEntry 0 0 1])
  gf2Groups <- expectRight (runHomologyBackend GF2RankBackend gf2Complex)
  assertEqual "GF2 backend acyclic interval" [0, 0] (fmap freeRank gf2Groups)
  assertEqual "GF2 backend tag" GF2RankBackendTag (homologyBackendTag GF2RankBackend)

testFieldBettiBasicComplexes :: Assertion
testFieldBettiBasicComplexes = do
  let isolatedPoints =
        mkFiniteChainComplex
          (HomologicalDegree 0)
          (const (emptyBoundaryIncidenceOf 4 0 :: BoundaryIncidence Rational))
  isolatedGroups <- rationalBetti isolatedPoints
  assertEqual "four isolated points H0" [4] (fmap freeRank isolatedGroups)
  assertEqual "degree cardinality comes from d0 source dimension" 4 (degreeCardinality isolatedPoints (HomologicalDegree 0))
  intervalComplex <- expectRight rationalIntervalComplex
  intervalGroups <- rationalBetti intervalComplex
  assertEqual "interval Betti" [1, 0] (fmap freeRank intervalGroups)

testFieldBettiRejectsInvalidChains :: Assertion
testFieldBettiRejectsInvalidChains = do
  malformedComplex <- expectRight malformedShapeComplex
  nonNilpotentValue <- expectRight nonNilpotentComplex
  case rationalBettiResult malformedComplex of
    Left (ChainComplexShapeMismatch 1 2 3) ->
      pure ()
    otherResult ->
      assertFailure ("expected malformed shape rejection, got " <> show otherResult)
  case rationalBettiResult nonNilpotentValue of
    Left (ChainComplexNilpotenceViolation 1) ->
      pure ()
    otherResult ->
      assertFailure ("expected non-nilpotent rejection, got " <> show otherResult)

testCheckedConstructorSeal :: Assertion
testCheckedConstructorSeal = do
  malformedComplex <- expectRight malformedShapeComplex
  nonNilpotentValue <- expectRight nonNilpotentComplex
  intervalComplex <- expectRight rationalIntervalComplex
  case resealComplex malformedComplex of
    Left (ChainComplexShapeMismatch 1 2 3) ->
      pure ()
    otherResult ->
      assertFailure ("expected shape refusal at the seal, got " <> show (() <$ otherResult))
  case resealComplex nonNilpotentValue of
    Left (ChainComplexNilpotenceViolation 1) ->
      pure ()
    otherResult ->
      assertFailure ("expected nilpotence refusal at the seal, got " <> show (() <$ otherResult))
  resealedInterval <- expectRight (resealComplex intervalComplex)
  intervalGroups <- rationalBetti resealedInterval
  assertEqual "sealed interval Betti" [1, 0] (fmap freeRank intervalGroups)

resealComplex :: FiniteChainComplex Rational -> Either HomologyFailure (FiniteChainComplex Rational)
resealComplex finite =
  mkFiniteChainComplexChecked (maxHomologicalDegree finite) (incidenceMatrixAt finite)

gf2Betti :: FiniteChainComplex GF2 -> IO [HomologyGroup GF2]
gf2Betti =
  expectRight . computeBettiNumbers (fieldBettiCapability GF2FieldRankBackend :: BettiCapability 'Phase2 GF2)

gf2OneBoundaryComplex ::
  Natural ->
  Natural ->
  [BoundaryEntry GF2] ->
  Either BoundaryIncidenceShapeError (FiniteChainComplex GF2)
gf2OneBoundaryComplex sourceDimension targetDimension entries = do
  boundaryIncidence <- mkBoundaryIncidence sourceDimension targetDimension entries
  pure $
    mkFiniteChainComplex (HomologicalDegree 1) $ \degreeValue ->
      case degreeValue of
        HomologicalDegree 1 ->
          boundaryIncidence
        HomologicalDegree 0 ->
          emptyBoundaryIncidenceOf targetDimension 0
        _ ->
          emptyBoundaryIncidence

rationalBetti :: FiniteChainComplex Rational -> IO [HomologyGroup Rational]
rationalBetti =
  expectRight . rationalBettiResult

rationalBettiResult :: FiniteChainComplex Rational -> Either HomologyFailure [HomologyGroup Rational]
rationalBettiResult =
  computeBettiNumbers (fieldBettiCapability RationalFieldRankBackend :: BettiCapability 'Phase2 Rational)

rationalIntervalComplex :: Either BoundaryIncidenceShapeError (FiniteChainComplex Rational)
rationalIntervalComplex = do
  intervalBoundary <-
    mkBoundaryIncidence
      1
      2
      [ mkBoundaryEntry 0 0 (-1),
        mkBoundaryEntry 0 1 1
      ]
  pure $
    mkFiniteChainComplex (HomologicalDegree 1) $ \degreeValue ->
      case degreeValue of
        HomologicalDegree 1 ->
          intervalBoundary
        HomologicalDegree 0 ->
          emptyBoundaryIncidenceOf 2 0
        _ ->
          emptyBoundaryIncidence

malformedShapeComplex :: Either BoundaryIncidenceShapeError (FiniteChainComplex Rational)
malformedShapeComplex = do
  malformedBoundary <- mkBoundaryIncidence 1 3 []
  pure $
    mkFiniteChainComplex (HomologicalDegree 1) $ \degreeValue ->
      case degreeValue of
        HomologicalDegree 1 ->
          malformedBoundary
        HomologicalDegree 0 ->
          emptyBoundaryIncidenceOf 2 0
        _ ->
          emptyBoundaryIncidence

nonNilpotentComplex :: Either BoundaryIncidenceShapeError (FiniteChainComplex Rational)
nonNilpotentComplex = do
  upperBoundary <- mkBoundaryIncidence 1 1 [mkBoundaryEntry 0 0 1]
  lowerBoundary <- mkBoundaryIncidence 1 1 [mkBoundaryEntry 0 0 1]
  pure $
    mkFiniteChainComplex (HomologicalDegree 2) $ \degreeValue ->
      case degreeValue of
        HomologicalDegree 2 ->
          upperBoundary
        HomologicalDegree 1 ->
          lowerBoundary
        HomologicalDegree 0 ->
          emptyBoundaryIncidenceOf 1 0
        _ ->
          emptyBoundaryIncidence

expectRight ::
  Show left =>
  Either left right ->
  IO right
expectRight result =
  case result of
    Left failureValue ->
      assertFailure (show failureValue)
    Right value ->
      pure value