algebraic-edge-graphs-0.1.1: src/EdgeGraph/Incidence/Internal.hs
{-# LANGUAGE CPP #-}
-----------------------------------------------------------------------------
-- |
-- Module : EdgeGraph.Incidence.Internal
-- Copyright : (c) Jack Liell-Cock 2025-2026
-- License : MIT (see the file LICENSE)
-- Maintainer : jackliellcock@gmail.com
-- Stability : unstable
--
-- This module exposes the implementation of flow representations (the canonical
-- representation for algebraic edge graphs) as described in the paper.
-- A flow representation is a set of nodes where each edge appears in
-- exactly one node's tips and exactly one node's pits, nodes can have empty
-- tips (source nodes) or empty pits (sink nodes) but not both empty, and
-- distinct nodes have disjoint tips and disjoint pits.
--
-- The API is unstable and unsafe. Where possible use the non-internal module
-- "EdgeGraph.Incidence" instead.
--
-----------------------------------------------------------------------------
module EdgeGraph.Incidence.Internal (
-- * Data structure
Node (..), Incidence (..), consistent,
-- * Normalization
normalize,
-- * Basic graph construction primitives
empty, edge, overlay, into, pits, tips, edges, fromNodeList, fromIncidenceList,
-- * Graph properties
nodeList, nodeSet, edgeSet, edgeList,
edgeCount, nodeCount, isEmpty, hasEdge,
-- * Graph transformation
removeEdge, detachPit, detachTip, gmap, induce
) where
#if !MIN_VERSION_base(4,20,0)
import Data.List (foldl')
#endif
import Data.Set (Set, union)
import qualified Data.IntMap.Strict as IntMap
import qualified Data.Map.Strict as Map
import qualified EdgeGraph.Class as C
import qualified Data.Set as Set
-- | A t'Node' represents an implicit vertex in an edge-indexed graph.
-- It has a set of incoming edges ('nodeTips') and a set of outgoing
-- edges ('nodePits'). A node may have empty tips (making it a source
-- node) or empty pits (making it a sink node), but not both empty.
data Node a = Node {
-- | The set of incoming edges (tips) for this node.
nodeTips :: Set a,
-- | The set of outgoing edges (pits) for this node.
nodePits :: Set a
} deriving (Eq, Ord)
instance (Ord a, Show a) => Show (Node a) where
show (Node ts ps) = "Node " ++ show (Set.toAscList ts) ++ " " ++ show (Set.toAscList ps)
-- | The t'Incidence' data type represents an edge-indexed graph as a flow
-- representation: a set of nodes where each edge appears in exactly one
-- node's tips and exactly one node's pits. This is the canonical representation
-- for algebraic edge graphs.
--
-- The 'Eq' instance satisfies all axioms of algebraic edge graphs.
newtype Incidence a = Incidence {
-- | The set of t'Node's in the flow representation.
nodes :: Set (Node a)
} deriving Eq
instance (Ord a, Show a) => Show (Incidence a) where
show (Incidence ns)
| Set.null ns = "empty"
| isSimpleEdge = "edge " ++ show singleLabel
| allIsolated = "edges " ++ show isolatedLabels
| otherwise = "fromNodeList " ++ show (Set.toAscList ns)
where
nl = Set.toAscList ns
isSimpleEdge = Set.size ns == 2 &&
case nl of
[Node ts1 ps1, Node ts2 ps2] ->
(Set.null ts1 && Set.size ps1 == 1 &&
Set.size ts2 == 1 && Set.null ps2 &&
ps1 == ts2) ||
(Set.size ts1 == 1 && Set.null ps1 &&
Set.null ts2 && Set.size ps2 == 1 &&
ts1 == ps2)
_ -> False
singleLabel = case nl of
[Node ts ps, _] | Set.null ts -> Set.findMin ps
[_, Node ts ps] | Set.null ts -> Set.findMin ps
_ -> error "singleLabel: not a simple edge"
-- Check all nodes are isolated source-sink pairs
allIsolated = even (length nl) &&
all (\(Node ts ps) -> Set.null ts || Set.null ps) nl &&
all (\(Node ts ps) -> Set.size ts <= 1 && Set.size ps <= 1) nl &&
length nl > 0
isolatedLabels = [Set.findMin ps | Node ts ps <- nl, Set.null ts, Set.size ps == 1]
instance Ord a => C.EdgeGraph (Incidence a) where
type Edge (Incidence a) = a
empty = empty
edge = edge
overlay = overlay
into = into
pits = pits
tips = tips
-- | Check if a flow representation is consistent:
--
-- 1. No (∅,∅) nodes
-- 2. Distinct nodes have disjoint tips
-- 3. Distinct nodes have disjoint pits
-- 4. The union of all tips equals the union of all pits (same edge set)
--
-- @
-- consistent 'EdgeGraph.Incidence.Internal.empty' == True
-- consistent ('edge' x) == True
-- consistent ('overlay' x y) == True
-- consistent ('into' x y) == True
-- @
consistent :: Ord a => Incidence a -> Bool
consistent (Incidence ns) = noEmptyNodes && disjointTips && disjointPits && sameDomain
where
nl = Set.toList ns
noEmptyNodes = all (\(Node ts ps) -> not (Set.null ts && Set.null ps)) nl
tipSizes = sum $ map (Set.size . nodeTips) nl
pitSizes = sum $ map (Set.size . nodePits) nl
allTipLabels = Set.unions $ map nodeTips nl
allPitLabels = Set.unions $ map nodePits nl
disjointTips = tipSizes == Set.size allTipLabels
disjointPits = pitSizes == Set.size allPitLabels
sameDomain = allTipLabels == allPitLabels
-- | Normalize a list of nodes into a valid flow representation by merging
-- nodes that transitively share any edge in their tips or pits.
-- Nodes that become (∅,∅) are removed.
--
-- This is the core algorithm that computes the least upper bound (overlay)
-- of a collection of nodes. It uses a union-find data structure indexed by
-- edges to efficiently determine which nodes must be merged.
normalize :: Ord a => [Node a] -> Set (Node a)
normalize [] = Set.empty
normalize rawNodes =
-- Phase 1: Index nodes and compute unions via label maps
let indexed = zip [0..] rawNodes
(_, _, uf) = foldl' processNode (Map.empty, Map.empty, IntMap.empty) indexed
-- Phase 2: Group nodes by their union-find representative and merge
groups = IntMap.fromListWith mergeNodes
[(ufFind uf i, n) | (i, n) <- indexed]
-- Phase 3: Filter out empty nodes
in Set.fromList [n | n <- IntMap.elems groups, nonEmptyNode n]
where
nonEmptyNode (Node ts ps) = not (Set.null ts && Set.null ps)
mergeNodes (Node t1 p1) (Node t2 p2) =
Node (t1 `Set.union` t2) (p1 `Set.union` p2)
-- For each node, register its labels in the tip/pit maps.
-- When a label is already mapped to another node, union them.
processNode (tipMap, pitMap, uf) (i, Node ts ps) =
let (tipMap', uf1) = foldl' (processLabel i) (tipMap, uf) (Set.toList ts)
(pitMap', uf2) = foldl' (processLabel i) (pitMap, uf1) (Set.toList ps)
in (tipMap', pitMap', uf2)
processLabel nodeIdx (labelMap, uf) label =
case Map.lookup label labelMap of
Nothing -> (Map.insert label nodeIdx labelMap, uf)
Just existingIdx -> (labelMap, ufUnion uf nodeIdx existingIdx)
-- | Find the root representative of an element in the union-find.
-- Uses path compression by following parent pointers to the root.
ufFind :: IntMap.IntMap Int -> Int -> Int
ufFind uf x = case IntMap.lookup x uf of
Nothing -> x
Just p | p == x -> x
| otherwise -> ufFind uf p
-- | Union two elements in the union-find structure.
-- Makes the root of x point to the root of y.
ufUnion :: IntMap.IntMap Int -> Int -> Int -> IntMap.IntMap Int
ufUnion uf x y =
let rx = ufFind uf x
ry = ufFind uf y
in if rx == ry then uf
else IntMap.insert rx ry uf
-- | Construct the /empty graph/.
-- Complexity: /O(1)/ time and memory.
--
-- @
-- 'isEmpty' empty == True
-- @
empty :: Incidence a
empty = Incidence Set.empty
-- | Construct the graph comprising /a single edge/.
-- An edge has two nodes: a source (with the label as a pit) and a sink
-- (with the label as a tip).
-- Complexity: /O(1)/ time and memory.
--
-- @
-- 'isEmpty' ('edge' x) == False
-- 'hasEdge' x ('edge' x) == True
-- 'edgeCount' ('edge' x) == 1
-- 'nodeCount' ('edge' x) == 2
-- @
edge :: Ord a => a -> Incidence a
edge a = Incidence $ Set.fromList
[ Node Set.empty (Set.singleton a) -- source: edge a leaves
, Node (Set.singleton a) Set.empty -- sink: edge a arrives
]
-- | /Overlay/ two graphs. This computes the least upper bound of two flow
-- representations by merging nodes that share edges.
-- Complexity: /O(n^2 * m)/ time.
--
-- @
-- 'isEmpty' ('overlay' x y) == 'isEmpty' x && 'isEmpty' y
-- 'overlay' 'EdgeGraph.Incidence.Internal.empty' x == x
-- 'overlay' x 'EdgeGraph.Incidence.Internal.empty' == x
-- 'overlay' x y == 'overlay' y x
-- 'overlay' x ('overlay' y z) == 'overlay' ('overlay' x y) z
-- 'overlay' x x == x
-- @
overlay :: Ord a => Incidence a -> Incidence a -> Incidence a
overlay (Incidence xs) (Incidence ys) =
Incidence $ normalize (Set.toList xs ++ Set.toList ys)
-- | Helper function c_i from Definition 10 of the paper.
-- Creates the intermediate nodes for the 'into' operation.
ci :: Ord a => a -> a -> [Node a]
ci d e
| d /= e = [ Node Set.empty (Set.singleton d)
, Node (Set.singleton d) (Set.singleton e)
, Node (Set.singleton e) Set.empty
]
| otherwise = [ Node (Set.singleton d) (Set.singleton d) ]
-- | /Into/ two graphs. Connects the sink side of the left graph to the
-- source side of the right graph, creating a sequential composition.
-- Complexity: /O((n + |E_l| * |E_r|)^2 * m)/ time.
--
-- @
-- 'isEmpty' ('into' x y) == 'isEmpty' x && 'isEmpty' y
-- 'into' 'EdgeGraph.Incidence.Internal.empty' x == x
-- 'into' x 'EdgeGraph.Incidence.Internal.empty' == x
-- 'into' ('edge' x) ('edge' y) /= 'overlay' ('edge' x) ('edge' y)
-- @
into :: Ord a => Incidence a -> Incidence a -> Incidence a
into x y = Incidence $ normalize allNodes
where
ds = edgeList x
es = edgeList y
ciNodes = concatMap (\d -> concatMap (ci d) es) ds
allNodes = Set.toList (nodes x) ++ Set.toList (nodes y) ++ ciNodes
-- | Helper function c_p from Definition 10 of the paper.
-- Creates the intermediate nodes for the 'pits' operation.
cp :: Ord a => a -> a -> [Node a]
cp d e = [ Node Set.empty (Set.fromList [d, e])
, Node (Set.singleton d) Set.empty
, Node (Set.singleton e) Set.empty
]
-- | /Pits/ two graphs. Connects where outgoing edges (pits) overlap,
-- causing source-side merging.
-- Complexity: /O((n + |E_l| * |E_r|)^2 * m)/ time.
--
-- @
-- 'isEmpty' ('pits' x y) == 'isEmpty' x && 'isEmpty' y
-- @
pits :: Ord a => Incidence a -> Incidence a -> Incidence a
pits x y = Incidence $ normalize allNodes
where
ds = edgeList x
es = edgeList y
cpNodes = concatMap (\d -> concatMap (cp d) es) ds
allNodes = Set.toList (nodes x) ++ Set.toList (nodes y) ++ cpNodes
-- | Helper function c_t from Definition 10 of the paper.
-- Creates the intermediate nodes for the 'tips' operation.
ct :: Ord a => a -> a -> [Node a]
ct d e = [ Node Set.empty (Set.singleton d)
, Node Set.empty (Set.singleton e)
, Node (Set.fromList [d, e]) Set.empty
]
-- | /Tips/ two graphs. Connects where incoming edges (tips) overlap,
-- causing sink-side merging.
-- Complexity: /O((n + |E_l| * |E_r|)^2 * m)/ time.
--
-- @
-- 'isEmpty' ('tips' x y) == 'isEmpty' x && 'isEmpty' y
-- @
tips :: Ord a => Incidence a -> Incidence a -> Incidence a
tips x y = Incidence $ normalize allNodes
where
ds = edgeList x
es = edgeList y
ctNodes = concatMap (\d -> concatMap (ct d) es) ds
allNodes = Set.toList (nodes x) ++ Set.toList (nodes y) ++ ctNodes
-- | Construct a graph from a given list of edges by overlaying them.
-- Complexity: /O(L^2 * log(L))/ time and /O(L)/ memory.
--
-- @
-- edges [] == 'EdgeGraph.Incidence.Internal.empty'
-- edges [x] == 'edge' x
-- @
edges :: Ord a => [a] -> Incidence a
edges = foldr overlay empty . map edge
-- | Construct a graph from a list of nodes. The nodes are normalized
-- (merged where they share labels).
-- Complexity: /O(L^2 * m)/ time and /O(L)/ memory.
fromNodeList :: Ord a => [Node a] -> Incidence a
fromNodeList = Incidence . normalize
-- | Construct a graph from a list of (tips, pits) pairs, where each pair
-- represents a node with its incoming edges (tips) and outgoing edge
-- labels (pits). The resulting graph is normalized (nodes sharing labels
-- are merged).
-- Complexity: /O(L^2 * m)/ time and /O(L)/ memory.
--
-- @
-- fromIncidenceList [] == 'EdgeGraph.Incidence.Internal.empty'
-- fromIncidenceList [([],[x]),([x],[])] == 'edge' x
-- @
fromIncidenceList :: Ord a => [([a], [a])] -> Incidence a
fromIncidenceList = fromNodeList . map (\(ts, ps) -> Node (Set.fromList ts) (Set.fromList ps))
-- | The sorted list of nodes of a graph.
-- Complexity: /O(n)/ time and /O(n)/ memory.
nodeList :: Incidence a -> [Node a]
nodeList = Set.toAscList . nodes
-- | The set of nodes of a graph.
nodeSet :: Incidence a -> Set (Node a)
nodeSet = nodes
-- | The number of nodes in a graph.
-- Complexity: /O(1)/ time.
nodeCount :: Incidence a -> Int
nodeCount = Set.size . nodes
-- | Check if a graph is empty.
-- Complexity: /O(1)/ time.
isEmpty :: Incidence a -> Bool
isEmpty = Set.null . nodes
-- | The set of all distinct edges appearing in any node of the graph.
-- Complexity: /O(n * m)/ time where n is the number of nodes and m is the
-- average number of labels per node.
edgeSet :: Ord a => Incidence a -> Set a
edgeSet (Incidence ns) = Set.unions
[ nodeTips n `union` nodePits n | n <- Set.toList ns ]
-- | The sorted list of all distinct edges.
edgeList :: Ord a => Incidence a -> [a]
edgeList = Set.toAscList . edgeSet
-- | The number of distinct edges.
edgeCount :: Ord a => Incidence a -> Int
edgeCount = Set.size . edgeSet
-- | Check if a graph contains a given edge.
hasEdge :: Ord a => a -> Incidence a -> Bool
hasEdge a = Set.member a . edgeSet
-- | Remove all occurrences of an edge from the graph. Nodes that become
-- (∅,∅) after removal are removed entirely.
-- Complexity: /O(n * log(n))/ time.
--
-- @
-- removeEdge x ('edge' x) == 'EdgeGraph.Incidence.Internal.empty'
-- @
removeEdge :: Ord a => a -> Incidence a -> Incidence a
removeEdge x (Incidence ns) = Incidence $ Set.fromList
[ n'
| n <- Set.toList ns
, let n' = Node (Set.delete x (nodeTips n)) (Set.delete x (nodePits n))
, not (Set.null (nodeTips n') && Set.null (nodePits n'))
]
-- | Detach an edge from its source node. The edge gets a fresh source
-- node @Node ∅ {a}@ while any other edges sharing the original source node
-- remain together. If the edge is already at its own source or is not in the
-- graph, this is a no-op.
-- Complexity: /O(n * log(n))/ time.
--
-- @
-- detachPit x ('edge' x) == 'edge' x
-- detachPit 2 ('into' ('edge' 1) ('edge' 2)) == 'edges' [1, 2]
-- detachPit 1 ('pits' ('edge' 1) ('edge' 2)) == 'edges' [1, 2]
-- @
detachPit :: Ord a => a -> Incidence a -> Incidence a
detachPit a r@(Incidence ns)
| not (hasEdge a r) = r
| otherwise = Incidence $ Set.insert freshSource stripped
where
freshSource = Node Set.empty (Set.singleton a)
stripped = Set.fromList
[ n'
| n <- Set.toList ns
, let n' = if Set.member a (nodePits n)
then Node (nodeTips n) (Set.delete a (nodePits n))
else n
, not (Set.null (nodeTips n') && Set.null (nodePits n'))
]
-- | Detach an edge from its sink node. The edge gets a fresh sink
-- node @Node {a} ∅@ while any other edges sharing the original sink node
-- remain together. If the edge is already at its own sink or is not in the
-- graph, this is a no-op.
-- Complexity: /O(n * log(n))/ time.
--
-- @
-- detachTip x ('edge' x) == 'edge' x
-- detachTip 1 ('into' ('edge' 1) ('edge' 2)) == 'edges' [1, 2]
-- detachTip 1 ('tips' ('edge' 1) ('edge' 2)) == 'edges' [1, 2]
-- @
detachTip :: Ord a => a -> Incidence a -> Incidence a
detachTip a r@(Incidence ns)
| not (hasEdge a r) = r
| otherwise = Incidence $ Set.insert freshSink stripped
where
freshSink = Node (Set.singleton a) Set.empty
stripped = Set.fromList
[ n'
| n <- Set.toList ns
, let n' = if Set.member a (nodeTips n)
then Node (Set.delete a (nodeTips n)) (nodePits n)
else n
, not (Set.null (nodeTips n') && Set.null (nodePits n'))
]
-- | Transform a graph by applying a function to each edge. The result
-- is normalized since the function may map different labels to the same value,
-- violating disjointness.
-- Complexity: /O(n^2 * m * log(m))/ time.
--
-- @
-- gmap f 'EdgeGraph.Incidence.Internal.empty' == 'EdgeGraph.Incidence.Internal.empty'
-- gmap f ('edge' x) == 'edge' (f x)
-- gmap id == id
-- gmap f . gmap g == gmap (f . g)
-- @
gmap :: (Ord a, Ord b) => (a -> b) -> Incidence a -> Incidence b
gmap f (Incidence ns) = Incidence $ normalize $ map mapNode $ Set.toList ns
where
mapNode (Node ts ps) = Node (Set.map f ts) (Set.map f ps)
-- | Construct the /induced subgraph/ of a given graph by removing edges
-- that do not satisfy a given predicate. Nodes that become (∅,∅) are removed.
-- Complexity: /O(n * m)/ time.
--
-- @
-- induce (const True) x == x
-- induce (const False) x == 'EdgeGraph.Incidence.Internal.empty'
-- @
induce :: Ord a => (a -> Bool) -> Incidence a -> Incidence a
induce p (Incidence ns) = Incidence $ Set.fromList
[ n'
| n <- Set.toList ns
, let n' = Node (Set.filter p (nodeTips n)) (Set.filter p (nodePits n))
, not (Set.null (nodeTips n') && Set.null (nodePits n'))
]