{-# LANGUAGE NoMonomorphismRestriction, TupleSections, DeriveFunctor #-}
module Math.Combinatorics.GraphAuts (isVertexTransitive, isEdgeTransitive,
isArcTransitive, is2ArcTransitive, is3ArcTransitive, is4ArcTransitive, isnArcTransitive,
isDistanceTransitive,
graphAuts, incidenceAuts, graphAuts7, graphAuts8, incidenceAuts2,
isGraphAut, isIncidenceAut,
graphIsos, incidenceIsos,
isGraphIso, isIncidenceIso) where
import Data.Either (lefts, rights, partitionEithers)
import qualified Data.List as L
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Data.Maybe
import Data.Ord (comparing)
import qualified Data.Foldable as Foldable
import qualified Data.Sequence as Seq
import Math.Common.ListSet
import Math.Core.Utils (combinationsOf, intersectAsc, pairs, picks, (^-))
import Math.Combinatorics.Graph
import Math.Algebra.Group.PermutationGroup
import Math.Algebra.Group.SchreierSims as SS
isVertexTransitive :: (Ord t) => Graph t -> Bool
isVertexTransitive (G [] []) = True
isVertexTransitive g@(G (v:vs) es) = orbitV auts v == v:vs where
auts = graphAuts g
isEdgeTransitive :: (Ord t) => Graph t -> Bool
isEdgeTransitive (G _ []) = True
isEdgeTransitive g@(G vs (e:es)) = orbitE auts e == e:es where
auts = graphAuts g
arc ->^ g = map (.^ g) arc
isArcTransitive :: (Ord t) => Graph t -> Bool
isArcTransitive (G _ []) = True
isArcTransitive g@(G vs es) = orbit (->^) a auts == a:as where
a:as = L.sort $ es ++ map reverse es
auts = graphAuts g
isArcTransitive' g@(G (v:vs) es) =
orbitP auts v == v:vs &&
orbitP stab n == n:ns
where auts = graphAuts g
stab = filter (\p -> v .^ p == v) auts
n:ns = nbrs g v
findArcs g@(G vs es) x l = map reverse $ dfs [ ([x],0) ] where
dfs ( (z1:z2:zs,l') : nodes)
| l == l' = (z1:z2:zs) : dfs nodes
| otherwise = dfs $ [(w:z1:z2:zs,l'+1) | w <- nbrs g z1, w /= z2] ++ nodes
dfs ( ([z],l') : nodes)
| l == l' = [z] : dfs nodes
| otherwise = dfs $ [([w,z],l'+1) | w <- nbrs g z] ++ nodes
dfs [] = []
isnArcTransitive :: (Ord t) => Int -> Graph t -> Bool
isnArcTransitive _ (G [] []) = True
isnArcTransitive n g@(G (v:vs) es) =
orbitP auts v == v:vs &&
orbit (->^) a stab == a:as
where auts = graphAuts g
stab = filter (\p -> v .^ p == v) auts
a:as = findArcs g v n
is2ArcTransitive :: (Ord t) => Graph t -> Bool
is2ArcTransitive g = isnArcTransitive 2 g
is3ArcTransitive :: (Ord t) => Graph t -> Bool
is3ArcTransitive g = isnArcTransitive 3 g
is4ArcTransitive :: (Ord t) => Graph t -> Bool
is4ArcTransitive g = isnArcTransitive 4 g
isDistanceTransitive :: (Ord t) => Graph t -> Bool
isDistanceTransitive (G [] []) = True
isDistanceTransitive g@(G (v:vs) es)
| isConnected g =
orbitP auts v == v:vs &&
length stabOrbits == diameter g + 1
| otherwise = error "isDistanceTransitive: only implemented for connected graphs"
where auts = graphAuts g
stab = filter (\p -> v .^ p == v) auts
stabOrbits = let os = orbits stab in os ++ map (:[]) ((v:vs) L.\\ concat os)
isGraphAut :: Ord t => Graph t -> Permutation t -> Bool
isGraphAut (G vs es) h = all (`S.member` es') [e -^ h | e <- es]
where es' = S.fromList es
isIncidenceAut :: (Ord p, Ord b) => Graph (Either p b) -> Permutation (Either p b) -> Bool
isIncidenceAut (G vs es) h = all (`S.member` es') [e ->^ h | e <- es]
where es' = S.fromList es
adjLists (G vs es) = adjLists' M.empty es
where adjLists' nbrs ([u,v]:es) =
adjLists' (M.insertWith (flip (++)) v [u] $ M.insertWith (flip (++)) u [v] nbrs) es
adjLists' nbrs [] = nbrs
data SearchTree a = T Bool a [SearchTree a] deriving (Eq, Ord, Show, Functor)
leftDepth (T _ _ []) = 1
leftDepth (T _ _ (t:ts)) = 1 + leftDepth t
leftWidths (T _ _ []) = []
leftWidths (T _ _ ts@(t:_)) = length ts : leftWidths t
graphAutsEdgeSearchTree (G vs es) = dfs [] vs vs where
dfs xys (x:xs) yys = T False xys [dfs ((x,y):xys) xs ys | (y,ys) <- picks yys, isCompatible xys (x,y)]
dfs xys [] [] = T True xys []
isCompatible xys (x',y') = and [([x,x'] `S.member` es') == (L.sort [y,y'] `S.member` es') | (x,y) <- xys]
es' = S.fromList es
graphAuts1 = map fromPairs . terminals . graphAutsEdgeSearchTree
terminals (T False _ ts) = concatMap terminals ts
terminals (T True xys _) = [xys]
transversalTerminals (T False _ (t:ts)) = concatMap (take 1 . transversalTerminals) ts ++ transversalTerminals t
transversalTerminals (T True xys _) = [xys]
transversalTerminals _ = []
graphAuts2 = filter (/=1) . map fromPairs . transversalTerminals . graphAutsEdgeSearchTree
isSingleton [_] = True
isSingleton _ = False
intersectCells p1 p2 = concat [ [c1 `intersectAsc` c2 | c2 <- p2] | c1 <- p1]
graphAutsDistancePartitionSearchTree g@(G vs es) = dfs [] ([vs],[vs]) where
dfs xys (srcPart,trgPart)
| all isSingleton srcPart =
let xys' = zip (concat srcPart) (concat trgPart)
in T (isCompatible xys') (xys++xys') []
| otherwise = let (x:xs):srcCells = srcPart
yys :trgCells = trgPart
srcPart' = intersectCells (xs : srcCells) (dps M.! x)
in T False xys
[dfs ((x,y):xys) ((unzip . L.sort) (zip (filter (not . null) srcPart') (filter (not . null) trgPart')))
| (y,ys) <- picks yys,
let trgPart' = intersectCells (ys : trgCells) (dps M.! y),
map length srcPart' == map length trgPart']
isCompatible xys = and [([x,x'] `S.member` es') == (L.sort [y,y'] `S.member` es') | (x,y) <- xys, (x',y') <- xys, x < x']
es' = S.fromList es
dps = M.fromAscList [(v, distancePartitionS vs es' v) | v <- vs]
graphAuts3 = filter (/=1) . map fromPairs . transversalTerminals . graphAutsDistancePartitionSearchTree
strongTerminals = strongTerminals' [] where
strongTerminals' gs (T False xys ts) =
case listToMaybe $ reverse $ filter (\(x,y) -> x /= y) xys of
Nothing -> L.foldl' (\hs t -> strongTerminals' hs t) gs ts
Just (x,y) -> if y `elem` (x .^^ gs)
then gs
else find1New gs ts
strongTerminals' gs (T True xys []) = fromPairs xys : gs
find1New gs (t:ts) = let hs = strongTerminals' gs t
in if take 1 gs /= take 1 hs
then hs
else find1New gs ts
find1New gs [] = gs
graphAuts :: (Ord a) => Graph a -> [Permutation a]
graphAuts = filter (/=1) . strongTerminals . graphAutsDistancePartitionSearchTree
graphAutsDistanceColouringSearchTree g@(G vs es) = dfs [] unitCol unitCol where
unitCol = (M.fromList $ map (,[]) vs, M.singleton [] vs)
dfs xys srcColouring@(srcVmap,srcCmap) trgColouring@(trgVmap,trgCmap)
| all isSingleton (M.elems srcCmap) =
let xys' = zip (concat $ M.elems srcCmap) (concat $ M.elems trgCmap)
in T (isCompatible xys') (reverse xys'++xys) []
| otherwise = let (x,c) = M.findMin srcVmap
(xVmap,xCmap) = dcs M.! x
ys = trgCmap M.! c
srcVmap' = M.delete x (intersectColouring srcVmap xVmap)
srcCmap' = colourPartition srcVmap'
in T False xys
[dfs ((x,y):xys) (srcVmap',srcCmap') (trgVmap',trgCmap')
| y <- ys,
let (yVmap,yCmap) = dcs M.! y,
let trgVmap' = M.delete y (intersectColouring trgVmap yVmap),
let trgCmap' = colourPartition trgVmap',
M.map length srcCmap' == M.map length trgCmap' ]
isCompatible xys = and [([x,x'] `S.member` es') == (L.sort [y,y'] `S.member` es') | (x,y) <- xys, (x',y') <- xys, x < x']
es' = S.fromList es
dcs = M.fromAscList [(v, distanceColouring v) | v <- vs]
distanceColouring u = let dp = distancePartitionS vs es' u
vmap = M.fromList [(v,[c]) | (cell,c) <- zip dp [0..], v <- cell]
cmap = M.fromList $ zip (map (:[]) [0..]) dp
in (vmap, cmap)
distanceColouring (G vs es) u = M.fromList [(v,[c]) | (cell,c) <- zip (distancePartitionS vs es' u) [0..], v <- cell]
where es' = S.fromList es
intersectColouring c1 c2 = M.intersectionWith (++) c1 c2
colourPartition c = L.foldr (\(k,v) m -> M.insertWith (++) v [k] m) M.empty (M.assocs c)
equitableRefinement g@(G vs es) p = equitableRefinement' (S.fromList es) p
equitableRefinement' edgeset partition = go partition where
go cells = let splits = L.zip (L.inits cells) (L.tails cells)
shatterPairs = [(L.zip ci counts,ls,rs) | (ls,ci:rs) <- splits, cj <- cells,
let counts = map (nbrCount cj) ci, isShatter counts]
in case shatterPairs of
[] -> cells
(vcs,ls,rs):_ -> let fragments = shatter vcs
in go (ls ++ fragments ++ rs)
isShatter (c:cs) = any (/= c) cs
shatter vcs = map (map fst) $ L.groupBy (\x y -> snd x == snd y) $ L.sortBy (comparing snd) $ vcs
nbrCount cell vertex = length (filter (isEdge vertex) cell)
isEdge u v = L.sort [u,v] `S.member` edgeset
equitablePartitionSearchTree g@(G vs es) p = dfs [] p where
dfs bs p = let p' = equitableRefinement' es' p in
if all isSingleton p'
then T True (p',bs) []
else T False (p',bs) [dfs (b:bs) p'' | (b,p'') <- splits [] p']
splits ls (r:rs) | isSingleton r = splits (r:ls) rs
| otherwise = let ls' = reverse ls in [(x, ls' ++ [x]:xs:rs) | (x,xs) <- picks r]
es' = S.fromList es
equitablePartitionSearchTree2 g@(G vs es) p = dfs [] ([],p) where
dfs bs (ss,cs) = let (ss',cs') = L.partition isSingleton $ equitableRefinement' es' cs
ss'' = ss++ss'
in case cs' of
[] -> T True (ss'',bs) []
c:cs'' -> T False (cs'++ss'',bs) [dfs (x:bs) (ss'',[x]:xs:cs'') | (x,xs) <- picks c]
es' = S.fromList es
equitableDistancePartitionSearchTree g@(G vs es) p = dfs [] p where
dfs bs p = let p' = equitableRefinement' es' p in
if all isSingleton p'
then T True (p',bs) []
else T False (p',bs) [dfs (b:bs) p'' | (b,p'') <- splits [] p']
splits ls (r:rs) | isSingleton r = splits (r:ls) rs
| otherwise = [(x, p'') | let ls' = reverse ls,
(x,xs) <- picks r,
let p' = ls' ++ [x]:xs:rs,
let p'' = filter (not . null) (intersectCells p' (dps M.! x))]
es' = S.fromList es
dps = M.fromAscList [(v, distancePartitionS vs es' v) | v <- vs]
trace1 p = map (\xs@(x:_) -> (x, length xs)) $ L.group $ L.sort $ map length p
equitablePartitionGraphSearchTree g@(G vs es) = equitablePartitionSearchTree g unitPartition
where unitPartition = [vs]
equitablePartitionIncidenceSearchTree g@(G vs es) = equitablePartitionSearchTree g lrPartition
where (lefts, rights) = partitionEithers vs
lrPartition = [map Left lefts, map Right rights]
leftLeaf (T False _ (t:ts)) = leftLeaf t
leftLeaf (T True (p,bs) []) = (concat p, reverse bs)
allLeaves (T False _ ts) = concatMap allLeaves ts
allLeaves (T True (p,bs) []) = [(concat p, reverse bs)]
partitionBSGS0 g@(G vs es) t = (bs, findLevels t) where
(p1,bs) = leftLeaf t
g1 = fromPairs $ zip p1 vs
g1' = g1^-1
es1 = S.fromList $ edges $ fmap (.^ g1) g
findLevels (T True (partition,_) []) = []
findLevels (T False (partition,_) (t:ts)) =
let hs = findLevels t
cell@(v:vs) = head $ filter (not . isSingleton) partition
in findLevel v hs (zip vs ts)
findLevel v hs ((v',t'):vts) = if v' `elem` v .^^ hs
then findLevel v hs vts
else let h = find1New t' in findLevel v (h++hs) vts
findLevel _ hs [] = hs
find1New (T False _ ts) = take 1 $ concatMap find1New ts
find1New (T True (partition,_) []) = let h = fromPairs $ zip (concat partition) vs
g' = fmap (.^ h) g
in if all (`S.member` es1) (edges g') then [h*g1'] else []
partitionBSGS g@(G vs es) t = (bs, findLevels t) where
(canonical,bs) = leftLeaf t
findLevels (T True (partition,_) []) = []
findLevels (T False (partition,_) (t:ts)) =
let hs = findLevels t
cell@(v:vs) = head $ filter (not . isSingleton) partition
in findLevel v hs (zip vs ts)
findLevel v hs ((v',t'):vts) = if v' `elem` v .^^ hs
then findLevel v hs vts
else let h = find1New t' in findLevel v (h++hs) vts
findLevel _ hs [] = hs
find1New (T False _ ts) = take 1 $ concatMap find1New ts
find1New (T True (partition,_) []) = let h = fromPairs $ zip canonical (concat partition)
in filter isAut [h]
isAut h = all (`S.member` es') [e -^ h | e <- es]
es' = S.fromList es
partitionBSGS3 g@(G vs es) t = (bs, findLevels t) where
(p1,bs) = leftLeaf t
findLevels (T True (partition,_) []) = []
findLevels (T False (partition,_) (t:ts)) =
let hs = findLevels t
cell@(v:vs) = head $ filter (not . isSingleton) partition
in findLevel v hs (zip vs ts)
findLevel v hs ((v',t'):vts) = if v' `elem` v .^^ rights hs
then findLevel v hs vts
else let h = find1New t' in findLevel v (h++hs) vts
findLevel _ hs [] = hs
find1New (T False _ ts) = take 1 $ concatMap find1New ts
find1New (T True (partition,_) []) = let h = fromPairs $ zip p1 (concat partition)
in if isAut h then [Right h] else [Left h]
isAut h = all (`S.member` es') [e -^ h | e <- es]
es' = S.fromList es
partitionBSGS2 g@(G vs es) t = (bs, findLevels t') where
t' = fmap (\(p,bs) -> (p,bs,trace1 p)) t
trace1 = length
(canonical,bs) = leftLeaf t
findLevels (T True (partition,_,_) []) = []
findLevels (T False (partition,_,_) (t:ts)) =
let (T _ (_,_,trace) _) = t
hs = findLevels t
cell@(v:vs) = head $ filter (not . isSingleton) partition
vts = filter (\(_,T _ (_,_,trace') _) -> trace == trace') $ zip vs ts
in findLevel v hs vts
findLevel v hs ((v',t'):vts) = if v' `elem` v .^^ hs
then findLevel v hs vts
else let h = find1New t' in findLevel v (h++hs) vts
findLevel _ hs [] = hs
find1New (T False _ ts) = take 1 $ concatMap find1New ts
find1New (T True (partition,_,_) []) = let h = fromPairs $ zip canonical (concat partition)
in filter isAut [h]
isAut h = all (`S.member` es') [e -^ h | e <- es]
es' = S.fromList es
graphAuts7 g = (partitionBSGS g) (equitablePartitionGraphSearchTree g)
graphAuts8 g = (partitionBSGS g) (equitableDistancePartitionSearchTree g [vertices g])
g1 = G [1..10] [[1,2],[1,3],[1,9],[2,4],[2,10],[3,4],[3,9],[4,10],[5,6],[5,8],[5,9],[6,7],[6,10],[7,8],[7,9],[8,10]]
g1' = nf $ fmap (\x -> if x <= 4 then x+4 else if x <= 8 then x-4 else x) g1
g2 = G [1..12] [[1,2],[1,4],[1,11],[2,3],[2,12],[3,4],[3,11],[4,12],[5,6],[5,8],[5,11],[6,9],[6,12],[7,8],[7,10],[7,11],[8,12],[9,10],[9,11],[10,12]]
maybeGraphIso g1 g2 = let (vs1,_) = (leftLeaf . equitablePartitionGraphSearchTree) g1
(vs2,_) = (leftLeaf . equitablePartitionGraphSearchTree) g2
f = M.fromList (zip vs1 vs2)
in if length vs1 == length vs2 && (nf . fmap (f M.!)) g1 == g2 then Just f else Nothing
incidenceAutsDistancePartitionSearchTree g@(G vs es) = dfs [] (lrPart, lrPart) where
dfs xys (srcPart,trgPart)
| all isSingleton srcPart =
let xys' = zip (concat srcPart) (concat trgPart)
in T (isCompatible xys') (unLeft $ xys++xys') []
| otherwise = let (x:xs):srcCells = srcPart
yys :trgCells = trgPart
srcPart' = intersectCells (xs : srcCells) (dps M.! x)
in T False (unLeft xys)
[dfs ((x,y):xys) ((unzip . L.sort) (zip (filter (not . null) srcPart') (filter (not . null) trgPart')))
| (y,ys) <- picks yys,
let trgPart' = intersectCells (ys : trgCells) (dps M.! y),
map length srcPart' == map length trgPart']
isCompatible xys = and [([x,x'] `S.member` es') == (L.sort [y,y'] `S.member` es') | (x,y) <- xys, (x',y') <- xys, x < x']
(lefts, rights) = partitionEithers vs
lrPart = [map Left lefts, map Right rights]
unLeft xys = [(x,y) | (Left x, Left y) <- xys]
es' = S.fromList es
dps = M.fromList [(v, distancePartitionS vs es' v) | v <- vs]
incidenceAuts :: (Ord p, Ord b) => Graph (Either p b) -> [Permutation p]
incidenceAuts = filter (/= p []) . strongTerminals . incidenceAutsDistancePartitionSearchTree
incidenceAuts2 g = (partitionBSGS g) (equitablePartitionIncidenceSearchTree g)
where unLeft (Left x) = x
graphIsos g1 g2
| length cs1 /= length cs2 = []
| otherwise = graphIsos' cs1 cs2
where cs1 = map (inducedSubgraph g1) (components g1)
cs2 = map (inducedSubgraph g2) (components g2)
graphIsos' (ci:cis) cjs =
[iso ++ iso' | (cj,cjs') <- picks cjs,
iso <- graphIsosCon ci cj,
iso' <- graphIsos' cis cjs']
graphIsos' [] [] = [[]]
graphIsosCon g1 g2
| isConnected g1 && isConnected g2
= concat [dfs [] (distancePartition g1 v1) (distancePartition g2 v2)
| v1 <- take 1 (vertices g1), v2 <- vertices g2]
| otherwise = error "graphIsosCon: either or both graphs are not connected"
where dfs xys p1 p2
| map length p1 /= map length p2 = []
| otherwise =
let p1' = filter (not . null) p1
p2' = filter (not . null) p2
in if all isSingleton p1'
then let xys' = xys ++ zip (concat p1') (concat p2')
in if isCompatible xys' then [xys'] else []
else let (x:xs):p1'' = p1'
ys:p2'' = p2'
in concat [dfs ((x,y):xys)
(intersectCells (xs : p1'') (dps1 M.! x))
(intersectCells (ys': p2'') (dps2 M.! y))
| (y,ys') <- picks ys]
isCompatible xys = and [([x,x'] `S.member` es1) == (L.sort [y,y'] `S.member` es2) | (x,y) <- xys, (x',y') <- xys, x < x']
dps1 = M.fromAscList [(v, distancePartitionS vs1 es1 v) | v <- vs1]
dps2 = M.fromAscList [(v, distancePartitionS vs2 es2 v) | v <- vs2]
vs1 = vertices g1
vs2 = vertices g2
es1 = S.fromList $ edges g1
es2 = S.fromList $ edges g2
isGraphIso :: (Ord a, Ord b) => Graph a -> Graph b -> Bool
isGraphIso g1 g2 = (not . null) (graphIsos g1 g2)
incidenceIsos g1 g2
| length cs1 /= length cs2 = []
| otherwise = incidenceIsos' cs1 cs2
where cs1 = map (inducedSubgraph g1) (filter (not . null . lefts) $ components g1)
cs2 = map (inducedSubgraph g2) (filter (not . null . lefts) $ components g2)
incidenceIsos' (ci:cis) cjs =
[iso ++ iso' | (cj,cjs') <- picks cjs,
iso <- incidenceIsosCon ci cj,
iso' <- incidenceIsos' cis cjs']
incidenceIsos' [] [] = [[]]
incidenceIsosCon g1 g2
| isConnected g1 && isConnected g2
= concat [dfs [] (distancePartition g1 v1) (distancePartition g2 v2)
| v1@(Left _) <- take 1 (vertices g1), v2@(Left _) <- vertices g2]
| otherwise = error "incidenceIsos: one or both graphs not connected"
where dfs xys p1 p2
| map length p1 /= map length p2 = []
| otherwise =
let p1' = filter (not . null) p1
p2' = filter (not . null) p2
in if all isSingleton p1'
then let xys' = xys ++ zip (concat p1') (concat p2')
in if isCompatible xys' then [[(x,y) | (Left x, Left y) <- xys']] else []
else let (x:xs):p1'' = p1'
ys:p2'' = p2'
in concat [dfs ((x,y):xys)
(intersectCells (xs : p1'') (dps1 M.! x))
(intersectCells (ys': p2'') (dps2 M.! y))
| (y,ys') <- picks ys]
isCompatible xys = and [([x,x'] `S.member` es1) == (L.sort [y,y'] `S.member` es2) | (x,y) <- xys, (x',y') <- xys, x < x']
dps1 = M.fromList [(v, distancePartition g1 v) | v <- vertices g1]
dps2 = M.fromList [(v, distancePartition g2 v) | v <- vertices g2]
es1 = S.fromList $ edges g1
es2 = S.fromList $ edges g2
isIncidenceIso :: (Ord p1, Ord b1, Ord p2, Ord b2) =>
Graph (Either p1 b1) -> Graph (Either p2 b2) -> Bool
isIncidenceIso g1 g2 = (not . null) (incidenceIsos g1 g2)