packages feed

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

module Moonlight.Homology.Pure.Topology.Realize
  ( Orientation (..),
    RawCellData (..),
    RawCellScopes (..),
    RealizationBudget (..),
    realizeScaffoldRaw,
    realizeScaffoldRawWithScopes,
  )
where

import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.Kind (Type)
import Data.List (sortBy)
import Data.Map.Strict qualified as Map
import Data.Ord (Down (..), comparing)
import Data.Set (Set)
import Data.Set qualified as Set
import Moonlight.Algebra.Pure.Orientation (Orientation (..))
import Moonlight.Homology.Pure.Failure (HomologyFailure (..))
import Moonlight.Homology.Pure.Topology.Graph.Algebra (connectedComponentsFromAdjacency)
import Moonlight.Homology.Pure.Topology.MacroScaffold
  ( BasisCellRef,
    HarmonicLoop (..),
    MacroScaffoldIR (..),
    MorseReebArc (..),
    MorseReebNode (..),
    MorseReebScaffold (..),
    PotentialValue,
    ReebArcId,
    ReebNodeId (..),
    unPotentialValue,
  )

type RawCellData :: Type
data RawCellData = RawCellData
  { rawVertices :: [Int],
    rawEdges :: [(Int, Int, Int)],
    rawFaces :: [(Int, [(Int, Orientation)])],
    rawVertexCount :: !Int,
    rawEdgeCount :: !Int,
    rawFaceCount :: !Int
  }
  deriving stock (Eq, Show)

instance Semigroup RawCellData where
  a <> b =
    RawCellData
      { rawVertices = rawVertices a <> rawVertices b,
        rawEdges = rawEdges a <> rawEdges b,
        rawFaces = rawFaces a <> rawFaces b,
        rawVertexCount = rawVertexCount a + rawVertexCount b,
        rawEdgeCount = rawEdgeCount a + rawEdgeCount b,
        rawFaceCount = rawFaceCount a + rawFaceCount b
      }

instance Monoid RawCellData where
  mempty = RawCellData [] [] [] 0 0 0

type RawCellScopes :: Type -> Type
data RawCellScopes tag = RawCellScopes
  { rawVertexScopes :: Map.Map tag (Set Int),
    rawEdgeScopes :: Map.Map tag (Set Int),
    rawFaceScopes :: Map.Map tag (Set Int)
  }
  deriving stock (Eq, Show)

instance Ord tag => Semigroup (RawCellScopes tag) where
  left <> right =
    RawCellScopes
      { rawVertexScopes = Map.unionWith Set.union (rawVertexScopes left) (rawVertexScopes right),
        rawEdgeScopes = Map.unionWith Set.union (rawEdgeScopes left) (rawEdgeScopes right),
        rawFaceScopes = Map.unionWith Set.union (rawFaceScopes left) (rawFaceScopes right)
      }

instance Ord tag => Monoid (RawCellScopes tag) where
  mempty =
    RawCellScopes
      { rawVertexScopes = Map.empty,
        rawEdgeScopes = Map.empty,
        rawFaceScopes = Map.empty
      }

type RealizationBudget :: Type
newtype RealizationBudget = RealizationBudget
  { unRealizationBudget :: Int
  }
  deriving stock (Eq, Show)

type IdState :: Type
data IdState = IdState
  { nextVertexId :: !Int,
    nextEdgeId :: !Int,
    nextFaceId :: !Int
  }

realizeScaffoldRaw :: MacroScaffoldIR -> RealizationBudget -> Either HomologyFailure RawCellData
realizeScaffoldRaw scaffold budget = do
  let reebScaffold = macroScaffoldReeb scaffold
      nodes = morseReebNodes reebScaffold
      arcs = morseReebArcs reebScaffold
      loops = macroScaffoldHarmonicLoops scaffold
      nodePotentialMap = buildNodePotentialMap nodes
  validateLoopClosure reebScaffold loops
  let (nodeData, nodeVertexMap, initialIdState) = realizeNodes reebScaffold
      arcBudgets = distributeBudget budget arcs nodePotentialMap
  arcDataList <- foldArcs nodeVertexMap arcBudgets initialIdState arcs
  Right (nodeData <> mconcat arcDataList)

realizeScaffoldRawWithScopes ::
  Ord tag =>
  (BasisCellRef -> Set tag) ->
  MacroScaffoldIR ->
  RealizationBudget ->
  Either HomologyFailure (RawCellData, RawCellScopes tag)
realizeScaffoldRawWithScopes labelsAt scaffold budget = do
  let reebScaffold = macroScaffoldReeb scaffold
      nodes = morseReebNodes reebScaffold
      arcs = morseReebArcs reebScaffold
      loops = macroScaffoldHarmonicLoops scaffold
      nodePotentialMap = buildNodePotentialMap nodes
  validateLoopClosure reebScaffold loops
  let (nodeData, nodeVertexMap, initialIdState, nodeScopes) =
        realizeNodesWithScopes labelsAt reebScaffold
      arcBudgets = distributeBudget budget arcs nodePotentialMap
  (arcData, arcScopes) <-
    foldArcsWithScopes labelsAt nodeVertexMap arcBudgets initialIdState arcs
  pure (nodeData <> arcData, nodeScopes <> arcScopes)

buildNodePotentialMap :: [MorseReebNode] -> Map.Map ReebNodeId PotentialValue
buildNodePotentialMap =
  Map.fromList
    . fmap (\node -> (morseReebNodeId node, morseReebNodePotential node))

realizeNodes :: MorseReebScaffold -> (RawCellData, Map.Map ReebNodeId Int, IdState)
realizeNodes reebScaffold =
  let nodes = morseReebNodes reebScaffold
      nodeCount = length nodes
      nodeVertexMap =
        Map.fromList
          ( zip
              (fmap morseReebNodeId nodes)
              [0 .. nodeCount - 1]
          )
      nodeData =
        RawCellData
          { rawVertices = [0 .. nodeCount - 1],
            rawEdges = [],
            rawFaces = [],
            rawVertexCount = nodeCount,
            rawEdgeCount = 0,
            rawFaceCount = 0
          }
      initialIdState =
        IdState
          { nextVertexId = nodeCount,
            nextEdgeId = 0,
            nextFaceId = 0
          }
   in (nodeData, nodeVertexMap, initialIdState)

realizeNodesWithScopes ::
  Ord tag =>
  (BasisCellRef -> Set tag) ->
  MorseReebScaffold ->
  (RawCellData, Map.Map ReebNodeId Int, IdState, RawCellScopes tag)
realizeNodesWithScopes labelsAt reebScaffold =
  let (nodeData, nodeVertexMap, initialIdState) = realizeNodes reebScaffold
      nodeScopes =
        morseReebNodes reebScaffold
          & zip [0 ..]
          & foldr
            ( \(vertexId, nodeValue) ->
                (<>)
                  (scopeForVertices (labelsAt (morseReebNodeAnchor nodeValue)) [vertexId])
            )
            mempty
   in (nodeData, nodeVertexMap, initialIdState, nodeScopes)

distributeBudget ::
  RealizationBudget ->
  [MorseReebArc] ->
  Map.Map ReebNodeId PotentialValue ->
  Map.Map ReebArcId Int
-- | Distribute a face budget across arcs proportionally to potential delta,
-- with a floor of one face per arc. The floor dominates the budget: when
-- @totalBudget < length arcs@ the distribution totals @length arcs@, i.e.
-- the budget is treated as a target, not a hard cap.
distributeBudget (RealizationBudget totalBudget) arcs nodePotentialMap =
  let arcDeltas =
        fmap (arcPotentialDelta nodePotentialMap) arcs
      totalDelta = sum arcDeltas
      arcIdsWithDeltas = zip (fmap morseReebArcId arcs) arcDeltas
      rawAllocations =
        fmap
          ( \(arcId, delta) ->
              let proportion =
                    if totalDelta > 0
                      then delta / totalDelta
                      else 1.0 / fromIntegral (max 1 (length arcs))
                  rawAlloc = fromIntegral totalBudget * proportion :: Double
                  floorAlloc = max 1 (floor rawAlloc :: Int)
                  fractionalRemainder = rawAlloc - fromIntegral floorAlloc
               in (arcId, floorAlloc, fractionalRemainder)
          )
          arcIdsWithDeltas
      floorTotal = sum (fmap (\(_, alloc, _) -> alloc) rawAllocations)
      deficit = totalBudget - floorTotal
      sortedByRemainder =
        sortBy (comparing (\(_, _, remainder) -> Down remainder)) rawAllocations
      distributed = distributeRemainder deficit sortedByRemainder
   in Map.fromList distributed

distributeRemainder :: Int -> [(ReebArcId, Int, Double)] -> [(ReebArcId, Int)]
distributeRemainder remaining allocations =
  case allocations of
    [] -> []
    (arcId, alloc, _remainder) : rest
      | remaining > 0 ->
          (arcId, alloc + 1) : distributeRemainder (remaining - 1) rest
      | otherwise ->
          (arcId, alloc) : fmap (\(aid, a, _) -> (aid, a)) rest

arcPotentialDelta :: Map.Map ReebNodeId PotentialValue -> MorseReebArc -> Double
arcPotentialDelta nodePotentialMap arc =
  let sourcePotential =
        maybe 0.0 unPotentialValue
          (Map.lookup (morseReebArcSource arc) nodePotentialMap)
      targetPotential =
        maybe 0.0 unPotentialValue
          (Map.lookup (morseReebArcTarget arc) nodePotentialMap)
   in abs (targetPotential - sourcePotential)

foldArcs ::
  Map.Map ReebNodeId Int ->
  Map.Map ReebArcId Int ->
  IdState ->
  [MorseReebArc] ->
  Either HomologyFailure [RawCellData]
foldArcs nodeVertexMap arcBudgets initialState arcs =
  let step (currentState, revAccumulated) arc = do
        (nextState, cellData) <- realizeArcWithState nodeVertexMap arcBudgets currentState arc
        Right (nextState, cellData : revAccumulated)
   in fmap (reverse . snd) (foldl' (\acc arc -> acc >>= \s -> step s arc) (Right (initialState, [])) arcs)

foldArcsWithScopes ::
  Ord tag =>
  (BasisCellRef -> Set tag) ->
  Map.Map ReebNodeId Int ->
  Map.Map ReebArcId Int ->
  IdState ->
  [MorseReebArc] ->
  Either HomologyFailure (RawCellData, RawCellScopes tag)
foldArcsWithScopes labelsAt nodeVertexMap arcBudgets initialState arcs =
  let step (currentState, revDataList, revScopesList) arc = do
        (nextState, cellData, cellScopes) <-
          realizeArcWithStateAndScopes labelsAt nodeVertexMap arcBudgets currentState arc
        Right (nextState, cellData : revDataList, cellScopes : revScopesList)
   in fmap
        (\(_, revDataList, revScopesList) -> (mconcat (reverse revDataList), mconcat (reverse revScopesList)))
        (foldl' (\acc arc -> acc >>= \stateValue -> step stateValue arc) (Right (initialState, [], [])) arcs)

realizeArcWithState ::
  Map.Map ReebNodeId Int ->
  Map.Map ReebArcId Int ->
  IdState ->
  MorseReebArc ->
  Either HomologyFailure (IdState, RawCellData)
realizeArcWithState nodeVertexMap arcBudgets idState arc = do
  let arcId = morseReebArcId arc
  faceBudget <-
    maybe
      (Left (InvalidTopologyInput "arc budget not found during realization"))
      Right
      (Map.lookup arcId arcBudgets)
  sourceVertex <-
    maybe
      (Left (InvalidTopologyInput "arc source node not found in vertex map"))
      Right
      (Map.lookup (morseReebArcSource arc) nodeVertexMap)
  targetVertex <-
    maybe
      (Left (InvalidTopologyInput "arc target node not found in vertex map"))
      Right
      (Map.lookup (morseReebArcTarget arc) nodeVertexMap)
  Right (realizeArc idState faceBudget sourceVertex targetVertex)

realizeArcWithStateAndScopes ::
  Ord tag =>
  (BasisCellRef -> Set tag) ->
  Map.Map ReebNodeId Int ->
  Map.Map ReebArcId Int ->
  IdState ->
  MorseReebArc ->
  Either HomologyFailure (IdState, RawCellData, RawCellScopes tag)
realizeArcWithStateAndScopes labelsAt nodeVertexMap arcBudgets idState arc = do
  (nextState, cellData) <- realizeArcWithState nodeVertexMap arcBudgets idState arc
  let arcLabels =
        morseReebArcSupport arc
          & fmap labelsAt
          & foldr Set.union Set.empty
  pure
    ( nextState,
      cellData,
      scopeForRawCellData arcLabels cellData
    )

realizeArc :: IdState -> Int -> Int -> Int -> (IdState, RawCellData)
realizeArc idState faceBudget sourceVertex targetVertex =
  let intermediateCount = faceBudget - 1
      intermediateVertexIds = [nextVertexId idState .. nextVertexId idState + intermediateCount - 1]
      lateralVertexId = nextVertexId idState + intermediateCount
      newVertexIds = intermediateVertexIds <> [lateralVertexId]
      newVertexCount = intermediateCount + 1
      spineVertices = [sourceVertex] <> intermediateVertexIds <> [targetVertex]
      spineEdges =
        zipWith
          (\i (src, tgt) -> (nextEdgeId idState + i, src, tgt))
          [0 ..]
          (zip (take faceBudget spineVertices) (drop 1 spineVertices))
      lateralEdgeStart = nextEdgeId idState + faceBudget
      lateralEdgeCount = faceBudget + 1
      lateralEdges =
        zipWith
          (\i sv -> (lateralEdgeStart + i, sv, lateralVertexId))
          [0 ..]
          spineVertices
      totalNewEdges = faceBudget + lateralEdgeCount
      faceStart = nextFaceId idState
      faces =
        zipWith
          ( \i faceIdx ->
              let spineEdgeId = nextEdgeId idState + i
                  lateralEdgeIdNext = lateralEdgeStart + i + 1
                  lateralEdgeIdCur = lateralEdgeStart + i
               in ( faceIdx,
                    [ (spineEdgeId, Positive),
                      (lateralEdgeIdNext, Positive),
                      (lateralEdgeIdCur, Negative)
                    ]
                  )
          )
          [0 .. faceBudget - 1]
          [faceStart .. faceStart + faceBudget - 1]
      nextState =
        IdState
          { nextVertexId = nextVertexId idState + newVertexCount,
            nextEdgeId = nextEdgeId idState + totalNewEdges,
            nextFaceId = nextFaceId idState + faceBudget
          }
      cellData =
        RawCellData
          { rawVertices = newVertexIds,
            rawEdges = spineEdges <> lateralEdges,
            rawFaces = faces,
            rawVertexCount = newVertexCount,
            rawEdgeCount = totalNewEdges,
            rawFaceCount = faceBudget
         }
   in (nextState, cellData)

scopeForRawCellData :: Ord tag => Set tag -> RawCellData -> RawCellScopes tag
scopeForRawCellData labelsValue rawCellData =
  scopeForVertices labelsValue (rawVertices rawCellData)
    <> scopeForEdges labelsValue (fmap (\(edgeId, _, _) -> edgeId) (rawEdges rawCellData))
    <> scopeForFaces labelsValue (fmap fst (rawFaces rawCellData))

scopeForVertices :: Ord tag => Set tag -> [Int] -> RawCellScopes tag
scopeForVertices labelsValue vertexIds =
  mempty
    { rawVertexScopes = labelsToCells labelsValue vertexIds
    }

scopeForEdges :: Ord tag => Set tag -> [Int] -> RawCellScopes tag
scopeForEdges labelsValue edgeIds =
  mempty
    { rawEdgeScopes = labelsToCells labelsValue edgeIds
    }

scopeForFaces :: Ord tag => Set tag -> [Int] -> RawCellScopes tag
scopeForFaces labelsValue faceIds =
  mempty
    { rawFaceScopes = labelsToCells labelsValue faceIds
    }

labelsToCells :: Ord tag => Set tag -> [Int] -> Map.Map tag (Set Int)
labelsToCells labelsValue cellIds =
  labelsValue
    & Set.toList
    & fmap (\labelValue -> (labelValue, Set.fromList cellIds))
    & Map.fromListWith Set.union

validateLoopClosure :: MorseReebScaffold -> [HarmonicLoop] -> Either HomologyFailure ()
validateLoopClosure reebScaffold loops =
  let arcs = morseReebArcs reebScaffold
      arcEndpointMap =
        Map.fromList
          ( fmap
              (\arc -> (morseReebArcId arc, (morseReebArcSource arc, morseReebArcTarget arc)))
              arcs
          )
   in traverse_ (validateSingleLoop arcEndpointMap) loops

validateSingleLoop ::
  Map.Map ReebArcId (ReebNodeId, ReebNodeId) ->
  HarmonicLoop ->
  Either HomologyFailure ()
validateSingleLoop arcEndpointMap loop =
  let supportArcs = harmonicLoopSupport loop
   in case supportArcs of
        [] -> Right ()
        _ ->
          let endpoints =
                fmap
                  (\arcId -> Map.lookup arcId arcEndpointMap)
                  supportArcs
           in case sequence endpoints of
                Nothing ->
                  Left
                    ( InvalidTopologyInput
                        "harmonic loop references arc not present in scaffold"
                    )
                Just pairs ->
                  validateArcChainClosure pairs

validateArcChainClosure :: [(ReebNodeId, ReebNodeId)] -> Either HomologyFailure ()
validateArcChainClosure pairs =
  let allEndpoints = concatMap (\(src, tgt) -> [src, tgt]) pairs
      occurrences =
        foldl'
          (\acc nodeId -> Map.insertWith (+) nodeId (1 :: Int) acc)
          Map.empty
          allEndpoints
      oddDegreeNodes =
        Map.filter odd occurrences
   in if not (Map.null oddDegreeNodes)
        then
          Left
            ( InvalidTopologyInput
                "harmonic loop arc support has nodes with odd degree"
            )
        else case Map.keys occurrences of
          [] -> Right ()
          (_ : _) ->
            let adjacency =
                  foldl'
                    ( \acc (src, tgt) ->
                        Map.insertWith Set.union src (Set.singleton tgt) (Map.insertWith Set.union tgt (Set.singleton src) acc)
                    )
                    Map.empty
                    pairs
                componentCount = connectedComponentsFromAdjacency adjacency
             in if componentCount == 1
                  then Right ()
                  else
                    Left
                      ( InvalidTopologyInput
                          "harmonic loop arc support is not connected"
                      )