packages feed

moonlight-category-0.1.0.0: test/simplicial/DeltaSpec.hs

module DeltaSpec
  ( tests,
  )
where

import Data.List (sort)
import Data.Function ((&))
import GHC.TypeNats (KnownNat)
import Numeric.Natural (Natural)
import Moonlight.Category.Simplicial
  ( Coface (..),
    Codegeneracy (..),
    DeltaMorphism,
    allDeltaMorphisms,
    cofaceMorphism,
    codegeneracyMorphism,
    composeDeltaMorphism,
    deltaIdentity,
    deltaDomainDimension,
    deltaCodomainDimension,
    deltaMapValues,
    denormalizeDeltaNormalForm,
    deltaMorphismEqual,
    injectionMissingIndices,
    mkDeltaMorphism,
    normalizeDeltaMorphism,
    surjectionDegeneracyIndices,
  )
import Moonlight.Category.Simplicial (Dimension (..), mkFinOffset)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, testCase)
import qualified Test.Tasty.QuickCheck as QC

genDeltaMorphism :: QC.Gen DeltaMorphism
genDeltaMorphism = do
  domainDimension <- QC.chooseInt (0, 4)
  codomainDimension <- QC.chooseInt (0, 4)
  sampledRow <- QC.vectorOf (domainDimension + 1) (QC.chooseInt (0, codomainDimension))
  let monotoneRow = sampledRow & sort & map fromIntegral
  case mkDeltaMorphism (fromIntegral domainDimension) (fromIntegral codomainDimension) monotoneRow of
    Nothing -> genDeltaMorphism
    Just morphism -> pure morphism

genComposableTriple :: QC.Gen (DeltaMorphism, DeltaMorphism, DeltaMorphism)
genComposableTriple = do
  nValue <- QC.chooseInt (0, 3)
  mValue <- QC.chooseInt (0, 3)
  kValue <- QC.chooseInt (0, 3)
  lValue <- QC.chooseInt (0, 3)
  case (allDeltaMorphisms (fromIntegral nValue) (fromIntegral mValue), allDeltaMorphisms (fromIntegral mValue) (fromIntegral kValue), allDeltaMorphisms (fromIntegral kValue) (fromIntegral lValue)) of
    ([], _, _) -> genComposableTriple
    (_, [], _) -> genComposableTriple
    (_, _, []) -> genComposableTriple
    (fCandidates, gCandidates, hCandidates) -> do
      fMorphism <- QC.elements fCandidates
      gMorphism <- QC.elements gCandidates
      hMorphism <- QC.elements hCandidates
      pure (hMorphism, gMorphism, fMorphism)

identityLawHolds :: DeltaMorphism -> Bool
identityLawHolds morphism =
  composeDeltaMorphism (deltaIdentity (deltaCodomainDimension morphism)) morphism == Just morphism
    && composeDeltaMorphism morphism (deltaIdentity (deltaDomainDimension morphism)) == Just morphism

associativityLawHolds :: (DeltaMorphism, DeltaMorphism, DeltaMorphism) -> Bool
associativityLawHolds (outer, middle, inner) =
  let leftComposed = composeDeltaMorphism outer =<< composeDeltaMorphism middle inner
      rightComposed = (\composed -> composeDeltaMorphism composed inner) =<< composeDeltaMorphism outer middle
   in case (leftComposed, rightComposed) of
        (Just leftValue, Just rightValue) -> deltaMorphismEqual leftValue rightValue
        (Nothing, Nothing) -> True
        _ -> False

normalizationRoundtripHolds :: DeltaMorphism -> Bool
normalizationRoundtripHolds morphism =
  case normalizeDeltaMorphism morphism >>= denormalizeDeltaNormalForm of
    Nothing -> False
    Just reconstructed -> deltaMorphismEqual morphism reconstructed


cofaceAt :: forall n. KnownNat n => Dimension n -> Natural -> Maybe DeltaMorphism
cofaceAt _ faceIndex =
  cofaceMorphism . CofaceMap <$> mkFinOffset @n @2 (Dimension @n) faceIndex

codegeneracyAt :: forall n. KnownNat n => Dimension n -> Natural -> Maybe DeltaMorphism
codegeneracyAt _ degeneracyIndex =
  codegeneracyMorphism . CodegeneracyMap <$> mkFinOffset @n @1 (Dimension @n) degeneracyIndex

composeMaybeDelta :: Maybe DeltaMorphism -> Maybe DeltaMorphism -> Maybe DeltaMorphism
composeMaybeDelta maybeOuter maybeInner = do
  outer <- maybeOuter
  inner <- maybeInner
  composeDeltaMorphism outer inner

assertDeltaCompositionEqual :: String -> Maybe DeltaMorphism -> Maybe DeltaMorphism -> IO ()
assertDeltaCompositionEqual label left right =
  assertEqual label left right

cofaceExample :: Maybe DeltaMorphism
cofaceExample =
  cofaceAt (Dimension @2) 1

codegeneracyExample :: Maybe DeltaMorphism
codegeneracyExample =
  codegeneracyAt (Dimension @2) 1

tests :: TestTree
tests =
  testGroup
    "Delta"
    [ testCase "coface generator maps into the next simplex dimension" $
        case cofaceExample of
          Nothing -> assertBool "expected coface morphism" False
          Just morphism -> do
            assertEqual "coface domain" 2 (deltaDomainDimension morphism)
            assertEqual "coface codomain" 3 (deltaCodomainDimension morphism)
            assertEqual "coface map" [0, 2, 3] (deltaMapValues morphism),
      testCase "codegeneracy generator collapses adjacent index" $
        case codegeneracyExample of
          Nothing -> assertBool "expected codegeneracy morphism" False
          Just morphism -> do
            assertEqual "codegeneracy domain" 3 (deltaDomainDimension morphism)
            assertEqual "codegeneracy codomain" 2 (deltaCodomainDimension morphism)
            assertEqual "codegeneracy map" [0, 1, 1, 2] (deltaMapValues morphism),
      testCase "coface generators satisfy the coface/coface identity" $
        assertDeltaCompositionEqual
          "δ₂δ₀ = δ₀δ₁"
          (composeMaybeDelta (cofaceAt (Dimension @2) 2) (cofaceAt (Dimension @1) 0))
          (composeMaybeDelta (cofaceAt (Dimension @2) 0) (cofaceAt (Dimension @1) 1)),
      testCase "codegeneracy generators satisfy the codegeneracy/codegeneracy identity" $
        assertDeltaCompositionEqual
          "σ₀σ₀ = σ₀σ₁"
          (composeMaybeDelta (codegeneracyAt (Dimension @1) 0) (codegeneracyAt (Dimension @2) 0))
          (composeMaybeDelta (codegeneracyAt (Dimension @1) 0) (codegeneracyAt (Dimension @2) 1)),
      testCase "mixed generators satisfy the left relation" $
        assertDeltaCompositionEqual
          "σ₁δ₀ = δ₀σ₀"
          (composeMaybeDelta (codegeneracyAt (Dimension @2) 1) (cofaceAt (Dimension @2) 0))
          (composeMaybeDelta (cofaceAt (Dimension @1) 0) (codegeneracyAt (Dimension @1) 0)),
      testCase "mixed generators collapse matching adjacent faces to identity" $ do
        assertDeltaCompositionEqual
          "σ₁δ₁ = id"
          (composeMaybeDelta (codegeneracyAt (Dimension @2) 1) (cofaceAt (Dimension @2) 1))
          (Just (deltaIdentity 2))
        assertDeltaCompositionEqual
          "σ₁δ₂ = id"
          (composeMaybeDelta (codegeneracyAt (Dimension @2) 1) (cofaceAt (Dimension @2) 2))
          (Just (deltaIdentity 2)),
      testCase "mixed generators satisfy the right relation" $
        assertDeltaCompositionEqual
          "σ₀δ₂ = δ₁σ₀"
          (composeMaybeDelta (codegeneracyAt (Dimension @2) 0) (cofaceAt (Dimension @2) 2))
          (composeMaybeDelta (cofaceAt (Dimension @1) 1) (codegeneracyAt (Dimension @1) 0)),
      testCase "mkDeltaMorphism rejects invalid length" $
        assertEqual "invalid length rejected" Nothing (mkDeltaMorphism 2 3 [0, 1]),
      testCase "mkDeltaMorphism rejects out-of-bounds values" $
        assertEqual "out-of-bounds value rejected" Nothing (mkDeltaMorphism 2 1 [0, 1, 2]),
      testCase "mkDeltaMorphism rejects non-monotone values" $
        assertEqual "non-monotone value rejected" Nothing (mkDeltaMorphism 2 3 [0, 2, 1]),
      testCase "degeneracy indices retain run positions for arbitrary rows" $
        assertEqual "run positions" [0, 2] (surjectionDegeneracyIndices [0, 0, 1, 0, 0]),
      testCase "injection complements ignore input order and duplicates" $
        assertEqual "missing indices" [0, 2, 4] (injectionMissingIndices 4 [3, 1, 1]),
      QC.testProperty "identity law for Delta morphisms" (QC.withNumTests 400 (QC.forAll genDeltaMorphism identityLawHolds)),
      QC.testProperty "associativity law for Delta morphism composition" (QC.withNumTests 400 (QC.forAll genComposableTriple associativityLawHolds)),
      QC.testProperty "normalization roundtrip preserves Delta morphism" (QC.withNumTests 400 (QC.forAll genDeltaMorphism normalizationRoundtripHolds))
    ]