packages feed

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

module NerveSpec
  ( carrierTests,
    lawfulCarrierSpec,
  )
where

import Data.Kind (Type)
import Data.Function ((&))
import Data.List (find)
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import Numeric.Natural (Natural)
import Moonlight.Category.Effect.Fixture.FinCat (sampleFinCat)
import Moonlight.Category
  ( ComposableChain,
    FinCat,
    FinGeneratorId (..),
    FinMorphismId (..),
    FinObjectId (..),
    allObjects,
    chainMorphisms,
    chainStartObject,
    chainsOfDimension,
    finMorId,
    finObjId,
    mkFinCat
  )
import Moonlight.Category.Simplicial
  ( NerveSimplex,
    applyFaceAtDimension,
    fillNerveInnerHorn,
    generatedSimplicesAtDimension,
    mkHorn,
    mkInnerHorn,
    nerve,
    nerveGenerated,
    nerveSimplexChain,
    nerveSimplexDimension,
    nerveSimplexFromChain,
    simplicesAtDimension,
  )
import Laws.Suite (LawSuiteConfig (..), mkLawfulCarrierSpec)
import Moonlight.Pale.Test.Laws.Suite (LawSuite)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertEqual, testCase)
import qualified Test.Tasty.QuickCheck as QC

type GeneratedFiniteCategory :: Type
data GeneratedFiniteCategory = GeneratedFiniteCategory
  { generatedCategory :: FinCat,
    generatedTruncation :: Natural
  }

instance Show GeneratedFiniteCategory where
  show generatedValue =
    "GeneratedFiniteCategory(objects="
      <> show (length (allObjects (generatedCategory generatedValue)))
      <> ", truncation="
      <> show (generatedTruncation generatedValue)
      <> ")"

instance QC.Arbitrary GeneratedFiniteCategory where
  arbitrary = do
    objectCount <- QC.chooseInt (1, 5)
    let objectIds = [0 .. objectCount - 1]
        candidatePairs = [(sourceId, targetId) | sourceId <- objectIds, targetId <- objectIds, sourceId < targetId]
    pairFlags <- QC.vectorOf (length candidatePairs) QC.arbitrary
    let chosenPairs =
          zip candidatePairs pairFlags
            & filter snd
            & map fst
        closurePairs = transitiveClosure (Set.fromList chosenPairs)
        morphismMap = buildMorphismMap closurePairs
        compositionMap = buildCompositionMap closurePairs
        categoryValue =
          case mkFinCat (Set.fromList (fmap objectId objectIds)) morphismMap compositionMap of
            Right validCategory -> validCategory
            Left _ -> sampleFinCat
    truncationValue <- QC.chooseInt (2, 4)
    pure
      ( GeneratedFiniteCategory
          { generatedCategory = categoryValue,
            generatedTruncation = fromIntegral truncationValue
          }
      )

pairCode :: Int -> Int -> Int
pairCode sourceId targetId = sourceId * 1024 + targetId

objectId :: Int -> FinObjectId
objectId = FinObjectId

generatorMorphismId :: Int -> FinMorphismId
generatorMorphismId = FinGeneratorMorphismId . FinGeneratorId

pairGenerator :: Int -> Int -> FinMorphismId
pairGenerator sourceId targetId = generatorMorphismId (pairCode sourceId targetId)

transitiveClosure :: Set.Set (Int, Int) -> Set.Set (Int, Int)
transitiveClosure relation =
  let composedPairs =
        Set.toList relation
          >>= (\(sourceId, middleId) -> Set.toList relation & filter (\(middleId', _) -> middleId == middleId') & map (\(_, targetId) -> (sourceId, targetId)))
          & Set.fromList
      nextRelation = Set.union relation composedPairs
   in if nextRelation == relation
        then relation
        else transitiveClosure nextRelation

buildMorphismMap :: Set.Set (Int, Int) -> Map.Map (FinObjectId, FinObjectId) [FinMorphismId]
buildMorphismMap relation =
  Set.toAscList relation
    & map (\(sourceId, targetId) -> ((objectId sourceId, objectId targetId), [pairGenerator sourceId targetId]))
    & Map.fromList

buildCompositionMap :: Set.Set (Int, Int) -> Map.Map (FinMorphismId, FinMorphismId) FinMorphismId
buildCompositionMap relation =
  let relationList = Set.toAscList relation
      compositionRows =
        relationList
          >>= ( \(sourceId, middleId) ->
                  relationList
                    & filter (\(middleId', _) -> middleId == middleId')
                    & map
                      (\(_, targetId) -> ((pairGenerator middleId targetId, pairGenerator sourceId middleId), pairGenerator sourceId targetId))
              )
   in Map.fromList compositionRows

singleMorphismId :: ComposableChain FinCat -> Maybe FinMorphismId
singleMorphismId chainValue =
  case chainMorphisms chainValue of
    [morphism] -> Just (finMorId morphism)
    _ -> Nothing

lookupSingleMorphismChain :: FinMorphismId -> [ComposableChain FinCat] -> Maybe (ComposableChain FinCat)
lookupSingleMorphismChain morphismId chains =
  find (\chainValue -> singleMorphismId chainValue == Just morphismId) chains

simplexFingerprint :: NerveSimplex FinCat -> (Natural, FinObjectId, [FinMorphismId])
simplexFingerprint simplexValue =
  let chainValue = nerveSimplexChain simplexValue
   in ( nerveSimplexDimension simplexValue,
        finObjId (chainStartObject chainValue),
        map finMorId (chainMorphisms chainValue)
      )

sameSimplex :: Maybe (NerveSimplex FinCat) -> Maybe (NerveSimplex FinCat) -> Bool
sameSimplex leftSimplex rightSimplex =
  case (leftSimplex, rightSimplex) of
    (Nothing, Nothing) -> True
    (Just leftValue, Just rightValue) -> simplexFingerprint leftValue == simplexFingerprint rightValue
    _ -> False

innerHornFillMatchesSimplex :: GeneratedFiniteCategory -> Bool
innerHornFillMatchesSimplex generatedValue =
  let simplicialSet = nerve (generatedCategory generatedValue) (generatedTruncation generatedValue)
   in and
        [ case mkHorn nerveSimplexDimension (applyFaceAtDimension simplicialSet) simplexDimension missingFace faceEntries of
            Left _ -> False
            Right hornValue ->
              case mkInnerHorn hornValue of
                Left _ -> False
                Right innerHornValue ->
                  sameSimplex
                    (fillNerveInnerHorn (generatedCategory generatedValue) innerHornValue)
                    (Just simplexValue)
          | simplexDimension <- [2 .. generatedTruncation generatedValue],
            simplexValue <- simplicesAtDimension simplicialSet simplexDimension,
            missingFace <- [1 .. simplexDimension - 1],
            let faceEntries =
                  [ (faceIndex, faceSimplex)
                    | faceIndex <- [0 .. simplexDimension],
                      faceIndex /= missingFace,
                      faceSimplex <- maybe [] pure (applyFaceAtDimension simplicialSet simplexDimension faceIndex simplexValue)
                  ],
            fromIntegral (length faceEntries) == simplexDimension
        ]

lawfulCarrierSpec :: LawSuite
lawfulCarrierSpec =
  mkLawfulCarrierSpec
    "nerve"
    LawSuiteConfig
      { lawSuiteName = "nerve simplicial laws",
        lawSuiteMaxSuccess = 300,
        lawSuiteCarrierToSSet = \generatedValue -> nerve (generatedCategory generatedValue) (generatedTruncation generatedValue),
        lawSuiteEquality = sameSimplex,
        lawSuiteRenderSimplex = show . simplexFingerprint
      }

carrierTests :: TestTree
carrierTests =
  testGroup
    "Nerve"
    [ testCase "generated and normalized carriers separate degenerate simplices" $ do
        let generatedSet = nerveGenerated sampleFinCat 1
            simplicialSet = nerve sampleFinCat 1
        assertEqual "generated 0-simplices" 3 (length (generatedSimplicesAtDimension generatedSet 0))
        assertEqual "generated 1-simplices" 6 (length (generatedSimplicesAtDimension generatedSet 1))
        assertEqual "normalized 0-simplices" 3 (length (simplicesAtDimension simplicialSet 0))
        assertEqual "normalized 1-simplices" 3 (length (simplicesAtDimension simplicialSet 1)),
      testCase "inner horn filler reconstructs a composable 2-simplex" $ do
        let oneChains = chainsOfDimension sampleFinCat 1
            maybeFirst = lookupSingleMorphismChain (generatorMorphismId 10) oneChains
            maybeSecond = lookupSingleMorphismChain (generatorMorphismId 11) oneChains
        case (maybeFirst, maybeSecond) of
          (Just firstChain, Just secondChain) ->
            let simplicialSet = nerve sampleFinCat 2
                hornEntries = [(2, nerveSimplexFromChain firstChain), (0, nerveSimplexFromChain secondChain)]
             in case mkHorn nerveSimplexDimension (applyFaceAtDimension simplicialSet) 2 1 hornEntries of
                  Left _ -> assertBool "expected a compatible inner horn" False
                  Right hornValue ->
                    case mkInnerHorn hornValue of
                      Left _ -> assertBool "expected an inner horn" False
                      Right innerHornValue ->
                        case fillNerveInnerHorn sampleFinCat innerHornValue of
                          Nothing -> assertBool "expected horn filler for nerve of sample category" False
                          Just simplexValue -> do
                            assertEqual "filled simplex dimension" 2 (nerveSimplexDimension simplexValue)
                            assertEqual "filled simplex chain length" 2 (length (chainMorphisms (nerveSimplexChain simplexValue)))
          _ -> assertBool "expected generator chains in dimension 1" False,
      QC.testProperty "inner horns reconstruct original simplex in generated nerves" $
        QC.withNumTests 200 innerHornFillMatchesSimplex
    ]