/* * Copyright 2022 Nina Andrup Pedersen * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */////// A library of functions that operates on graphs, represented as foldable/// collections of either pairs or triples, for unweighted and weighted graphs/// respectively. These pairs, or triples, represent directed edges while/// nodes(vertices) are given implicitly.///pubmod Graph {////// Returns the pairs `(a, b)` where `a` can reach `b` through a number of/// edges in the directed graph `g`, including zero.///pubdefclosure(g: m[(t, t)]): Set[(t, t)] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letres = queryedges, nodes(), reachability() select (src, dst) from Reachable(src, dst);Vector.toSet(res) }////// Returns the nodes that are reachable from `src` in the directed/// graph `g`.///pubdefreachableFrom(src: t, g: m[(t, t)]): Set[t] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letres = queryedges, reachabilityFromSrc(src) select dst from Reachable(dst);Vector.toSet(res) }////// Returns the nodes that are unreachable from `src` in the directed/// graph `g`.///pubdefunreachableFrom(src: t, g: m[(t, t)]): Set[t] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letunreachablility = #{// If there is a node that is not reachable from `src` then it is// unreachable. UnReachable(x) :- Node(x), not Reachable(x). };letres = queryedges, nodes(), reachabilityFromSrc(src), unreachablility select dst from UnReachable(dst);Vector.toSet(res) }////// Returns `true` if there is a path from `src` to `dst` in the directed/// graph `g`.///pubdefreachable(src: {src = t}, dst: {dst = t}, g: m[(t, t)]): Bool \ Foldable.Aef[m] withFoldable[m], Order[t] =reachableFrom(src#src, g) |> Set.exists(x -> dst#dst ==x)////// Returns the strongly connected components of the directed graph `g`./// Two nodes are in the same component if and only if they can both/// reach each other.///pubdefstronglyConnectedComponents(g: m[(t, t)]): Set[Set[t]] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letconnected = #{// If `n1` can reach `n2` and `n2` can reach `n1` then they are part// of the same strongly connected component. Connected(n1; Set#{n2}) :- Reachable(n1, n2), Reachable(n2, n1). };letcomponents = #{// After the full computation of `Connected`, duplicates are removed// by checking that `n` is the minimum in the strongly connected// component. Components(s) :- fix Connected(n; s), if (Some(n) ==Set.minimum(s)). };letres = queryedges, nodes(), reachability(), connected, components select x from Components(x);Vector.toSet(res) }////// Returns the graph where all edges in the directed graph `g` have their/// nodes flipped.///pubdefflipEdges(g: m[(t, t)]): Set[(t, t)] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letrev = #{ RevEdge(y, x) :- Edge(x, y). };letres = queryedges, rev select (x, y) from RevEdge(x, y);Vector.toSet(res) }////// Returns the inverse graph of the directed graph `g`. For all nodes in/// `g`. The new graph contains exactly those edges that are not in `g`.////// OBS: No self-edges are returned no matter the input.///pubdefinvert(g: m[(t, t)]): Set[(t, t)] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letinverse = #{ InvEdge(x, y) :- Node(x), Node(y), not Edge(x, y), if (x!=y). };letres = querynodes(), edges, inverse select (x, y) from InvEdge(x, y);Vector.toSet(res) }////// Returns `true` if the directed graph `g` contains at least one cycle.///pubdefisCyclic(g: m[(t, t)]): Bool \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letreachability = #{// Reachability given the edges. Reachable(n1, n2) :- Edge(n1, n2).// If `n1` can reach `m` and there is an edge from `m` to `n2` then// `n1` can also reach `n2`. Reachable(n1, n2) :- Reachable(n1, m), Edge(m, n2). };letres = queryedges, reachability select () from Reachable(x, y) where x == y;Vector.length(res) >0 }////// Returns the shortest distance between all pairs of nodes in the/// weighted directed graph `g`. Returns `None` if `g` contains a/// negative cycle.///pubdefdistances(g: m[(t, Int32, t)]): Option[Map[(t, t), Int32]] \ Foldable.Aef[m] withFoldable[m], Order[t] = {// Algorithm based on floyd-warshall./// Returns the number of items in `s` less than `v`.defrank(v, s) = s |> Set.partition(v0 -> v0<v) |> fst |> Set.size;letedges = injectginto Edge/3;letnegCycle = #{ Node(x) :- Edge(x, _, _). Node(x) :- Edge(_, _, x).// Collect all nodes. Nodes(; Set#{x}) :- Node(x).// Compute the number of nodes. NodeCount(Set.size(ns)) :- fix Nodes(; ns).// Compute the rank (the index in a sorted order) each node has. NodeRank(x, rank(x, ns)) :- Node(x), fix Nodes(; ns).// `Dist(x, y, k; w)` says that `x` can reach `y` with a path of// length `w` using only intermediate nodes of rank less than// `k`. The `k` parameter is to control the number of iterations.// Direct paths use no intermediate vertices. Dist(x, x, 0; Down.Down(0)) :- Node(x). Dist(x, y, 0; Down.Down(w)) :- Edge(x, w, y).// If there exists a path of length `w` without using node `k+1`// then that path is also valid using node `k+1`. Dist(x, y, k+1; w) :- Dist(x, y, k; w), NodeCount(c), if (k<c).// Combine paths if they use intermediate nodes of the correct// rank (also making sure `k` doesn't increase forever). Dist(x, y, k+1; w1+w2) :- Dist(x, kth, k; w1), Dist(kth, y, k; w2), NodeRank(kth, k), NodeCount(c),if (k<c).// If `x` can go to `x` with a negative distances, then it can be// repeated. NegCycle() :- fix Dist(x, x, _; w), if (destructDown(w) <0). };letmapping = #{ Mapping((x, y), destructDown(d)) :- fix Dist(x, y, c; d), NodeCount(c). };letsol = solveedges, negCycle, mapping;letcycle = querysol select () from NegCycle();if (Vector.isEmpty(cycle)) {// No negative cycle.letres = querysol select (p, d) from Mapping(p, d); Some(Vector.toMap(res)) } else {// Negative cycle. None } }////// Returns the shortest distance between all pairs of nodes in the/// weighted directed graph `g`.////// OBS: No negative cycles must be present.///pubdefboundedDistances(g: m[(t, Int32, t)]): Map[(t, t), Int32] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/3;letdists = #{// Initialize all node distances to zero. Dist(x, x; Down.Down(0)) :- Edge(x, _, _). Dist(x, x; Down.Down(0)) :- Edge(_, _, x).// Add distances from the edges. Dist(x, y; Down.Down(d)) :- Edge(x, d, y).// Add transitive distances. Dist(x, y; d1+ Down.Down(d2)) :- Dist(x, z; d1), Edge(z, d2, y). };letmapping = #{ Mapping((x, y), destructDown(d)) :- fix Dist(x, y; d). };letres = queryedges, dists, mapping select (p, d) from Mapping(p, d);res |> Vector.toMap }////// Returns the shortest distance from `src` to every other reachable vertex in the/// weighted directed graph `g`.///pubdefdistancesFrom(src: t, g: m[(t, Int32, t)]): Map[t, Int32] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/3;letdists = #{ Dist(src; Down.Down(0)). Dist(y; d+ Down.Down(w)) :- Dist(x; d), Edge(x, w, y). };letres = queryedges, distsselect (x, coerce(d)) from Dist(x; d);res |> Vector.toMap }////// Returns the shortest distance from `src` to `dst` in the weighted/// directed graph `g`.///pubdefdistance(src: {src = t}, dst: {dst = t}, g: m[(t, Int32, t)]): Option[Int32] \ Foldable.Aef[m] withFoldable[m], Order[t] =distancesFrom(src#src, g) |> Map.get(dst#dst)////// Returns a copy of the directed graph `g` where all flipped edges are/// added. An undirected graph in directed representation.///pubdeftoUndirected(g: m[(t, t)]): Set[(t, t)] \ Foldable.Aef[m] withFoldable[m], Order[t] = {g |> Foldable.toSet |> Set.flatMap(match (a, b) -> Set#{(a, b), (b, a)}) }////// Returns a copy of the weighted directed graph `g` where all flipped/// edges are added. An undirected graph in directed representation.///pubdeftoUndirectedLabeled(g: m[(t, Int32, t)]): Set[(t, Int32, t)] \ Foldable.Aef[m] withFoldable[m], Order[t] = {g |> Foldable.toSet |> Set.flatMap(match (a, w, b) -> Set#{(a, w, b), (b, w, a)}) }////// Returns the nodes that are at most `limit` (inclusive) distance away/// from `src` in the weighted directed graph `g`.////// OBS: No negative cycles must be present.///pubdefwithinDistanceOf(src: t, limit: Int32, g: m[(t, Int32, t)]): Set[t] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/3;letnodes = #{ Dist(src; Down.Down(0)) :- if (limit>=0). Dist(y; d+ Down.Down(w)) :- Dist(x; d), Edge(x, w, y),if (destructDown(d) +w<=limit). Node(x) :- fix Dist(x; _). };letres = queryedges, nodes select x from Node(x);Vector.toSet(res) }////// Returns the nodes that are at most `limit` (inclusive) edges away/// from `src` in the directed graph `g`.///pubdefwithinEdgesOf(src: t, limit: Int32, g: m[(t, t)]): Set[t] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letnodes = #{ Dist(src; Down.Down(0)) :- if (limit>=0). Dist(y; d+ Down.Down(1)) :- Dist(x; d), Edge(x, y),if (destructDown(d) +1<=limit). Node(x) :- fix Dist(x; _). };letres = queryedges, nodes select x from Node(x);Vector.toSet(res) }////// Returns the topologically sorted nodes (all edges go from lower indices/// to higher indices of the list) in the directed graph `g`./// Unordered nodes are consistently (although not intuitively) ordered.////// OBS: No cycles must be present.///pubdeftopologicalSort(g: m[(t, t)]): List[t] \ Foldable.Aef[m] withFoldable[m], Order[t] = regionrc {// https://github.com/souffle-lang/benchmarks/tree/master/benchmarks/topological_orderingletedges = injectginto Edge/2;lettopSort = #{ EdgeSecond(x) :- Edge(_, x). EdgeFirst(x) :- Edge(x, _). Before(x, y) :- Edge(x, y). Before(x, y) :- Before(x, z), Edge(z, y). After(x, y) :- Edge(y, x). After(x, y) :- After(z, y), Edge(z, x).// Find a consistent index for each node, not necessarily unique. Index(x; 0) :- Node(x), not EdgeSecond(x), EdgeFirst(x). Index(x; i+1) :- Before(y, x),not Before(x, y), After(x, y), Index(y; i). };letres = queryedges, nodes(), topSortselect (x, i) from Index(x; i);letml = MutList.empty(rc);// Extract nodes in fixed order.res |> Vector.sortBy(match (x, i) -> (i, x)) |> Vector.forEach(match (x, _) -> MutList.push(x, ml));MutList.toList(ml) }////// Returns the degree of each node in the directed graph `g` (the number of/// times a node exists as an endpoint of an edge).///pubdefdegrees(g: m[(t, t)]): Map[t, Int32] \ Foldable.Aef[m] withFoldable[m], Order[t] = {use Option.{getWithDefault, map};letedges = injectginto Edge/2;letin = #{// Careful use of option type here to make sure the empty set is not// the bottom lattice element. That would exclude degrees of zero. TouchSet(n; Some(Set#{})) :- Node(n). TouchSet(n; Some(Set#{(other, n)})) :- Edge(other, n). TouchSet(n; Some(Set#{(n, other)})) :- Edge(n, other). Degree(n, s |> map(Set.size) |> getWithDefault(0)) :-fix TouchSet(n; s). };letres = queryedges, in, nodes() select (n, d) from Degree(n, d);Vector.toMap(res) }////// Returns a mapping from distances to the set of nodes for which the/// shortest path from `src` in the directed graph `g` is of a given length.///pubdeffrontiersFrom(src: t, g: m[(t, t)]): Map[Int32, Set[t]] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;letfrontiers = #{ Dist(src; Down.Down(0)). Dist(y; n+ Down.Down(1)) :- Dist(x; n), Edge(x, y).// Find the max frontier. Frontiers(; Set#{destructDown(n)}) :- fix Dist(_; n). MaxFrontier(Set.maximum(s) |> Option.getWithDefault(0)) :- fix Frontiers(; s).// Initialize all frontiers (with non-bot element). Frontier(0; Some(Set#{})) :- MaxFrontier(m), if (m>0). Frontier(n+1; Some(Set#{})) :- Frontier(n; _), MaxFrontier(m), if (n<m).// Collect the frontiers. Frontier(destructDown(n); Some(Set#{x})) :- fix Dist(x; n). };letres = queryedges, frontiers select (n, s) from Frontier(n; s);letunwrapOption = match (n, s) -> matchs {case Some(v) => Some((n, v))case None => None };res |> Vector.filterMap(unwrapOption) |> Vector.toMap }////// Returns triples `(x, cut, y)` such that `x` cannot reach `y` without/// using `cut` (where `x`, `cut` and `y` are all distinct) in the directed/// graph `g`.////// There will at most be one triple for each pair of nodes (which will/// be the maximum `cut` of the possible choices).///pubdefcutPoints(g: m[(t, t)]): Set[(t, t, t)] \ Foldable.Aef[m] withFoldable[m], Order[t] = {letedges = injectginto Edge/2;defunpack(opt) = matchopt {case Some(v) => vcase None => unreachable!() };letcuts = #{ CutPoint(x, Set.maximum(cut) |> unpack, y) :-fix CutPointSet(x, y; cut),if (notSet.isEmpty(cut)). CutPointSet(x, y; Set#{cut}) :- Reachable(x, y), Node(cut),not Circumvent(x, cut, y),if (x!=cut),if (y!=cut),if (x!=y). Circumvent(x, cut, y) :- Node(cut), Edge(x, y),if (x!=cut),if (y!=cut). Circumvent(x, cut, y) :- Circumvent(x, cut, z), Edge(z, y), Node(cut),if (y!=cut). };letres = queryedges, nodes(), reachability(), cuts select (x, cut, y) from CutPoint(x, cut, y);Vector.toSet(res) }////// Returns the in-degree (how many edges end in a given node)/// of each node in the directed graph `g`.///pubdefinDegrees(g: m[(t, t)]): Map[t, Int32] \ Foldable.Aef[m] withFoldable[m], Order[t] = {use Option.{getWithDefault, map};letedges = injectginto Edge/2;letin = #{// Careful use of option type here to make sure the empty set is not// the bottom lattice element. That would exclude degrees of zero. InSet(n; Some(Set#{})) :- Node(n). InSet(n; Some(Set#{other})) :- Edge(other, n). InDegree(n, s |> map(Set.size) |> getWithDefault(0)) :-fix InSet(n; s). };letres = queryedges, in, nodes() select (n, d) from InDegree(n, d);Vector.toMap(res) }////// Returns the out-degree (how many edges start in a given node)/// of each node in the directed graph `g`.///pubdefoutDegrees(g: m[(t, t)]): Map[t, Int32] \ Foldable.Aef[m] withFoldable[m], Order[t] = {use Option.{getWithDefault, map};letedges = injectginto Edge/2;letout = #{// Careful use of option type here to make sure the empty set is not// the bottom lattice element. That would exclude degrees of zero. OutSet(n; Some(Set#{})) :- Node(n). OutSet(n; Some(Set#{other})) :- Edge(n, other). OutDegree(n, s |> map(Set.size) |> getWithDefault(0)) :-fix OutSet(n; s). };letres = queryedges, out, nodes() select (n, d) from OutDegree(n, d);Vector.toMap(res) }////// Returns a Graphviz (DOT) string of the directed graph `g`./// The strings of nodes are put in quotes but DOT identifier validity is/// up to the caller.///pubdeftoGraphviz(g: m[(t, t)]): String \ Foldable.Aef[m] withFoldable[m], Order[t], ToString[t] ="digraph {"+String.lineSeparator()+Foldable.joinWith(match (x, y) -> " ${graphvizId(x)} -> ${graphvizId(y)}" +String.lineSeparator(), "", g)+"}"+String.lineSeparator()////// Returns a Graphviz (DOT) string of the directed graph `g`./// The strings of nodes are put in quotes and existing quotes are escaped./// Other than that, DOT identifier validity is up to the caller.///pubdeftoGraphvizLabeled(g: m[(t, l, t)]): String \ Foldable.Aef[m] withFoldable[m], ToString[t], ToString[l] ="digraph {"+String.lineSeparator()+Foldable.joinWith(match (x, w, y) -> " ${graphvizId(x)} -> ${graphvizId(y)} [label = ${w}]"+String.lineSeparator(), "", g)+"}"+String.lineSeparator() // ------------------------------------------------------------------------- // Private Functions ------------------------------------------------------- // -------------------------------------------------------------------------////// Wraps `toString(id)` with double quotes and escapes any existing quotes/// with backslash.///defgraphvizId(id: t): StringwithToString[t] = {"\"${String.replace(src = "\"", dst = "\\\"", "${id}")}\"" }////// Returns a Datalog program which computes the reachable nodes when/// given a set of `Edge` and `Node` facts. Nodes can always reach/// themselves.///defreachability(): #{ Edge(t, t), Reachable(t, t), Node(t) | r } withOrder[t] = #{// All nodes can reach themselves. Reachable(n, n) :- Node(n).// If `n1` can reach `m` and there is an edge from `m` to `n2` then `n1`// can also reach `n2`. This adds all node pairs to the relational that// are reachable using any number of nodes. Reachable(n1, n2) :- Reachable(n1, m), Edge(m, n2). }////// Returns a Datalog program that, when given a set of `Edge` facts,/// computes the nodes.///defnodes(): #{ Edge(t, t), Node(t) | r } withOrder[t] = #{ Node(x) :- Edge(x, _). Node(x) :- Edge(_, x). }////// Returns a Datalog program which computes the reachable nodes from `src`/// when given a set of `Edge` facts.///defreachabilityFromSrc(src: t): #{ Edge(t, t), Reachable(t) | r } withOrder[t] = #{// A node can reach itself. Reachable(src).// If `src` can reach `m` and there is an edge from `m` to `n` then// `src` can also reach `n`. This adds all node pairs to the relational// that are reachable using any number of nodes. Reachable(n) :- Reachable(m), Edge(m, n). }////// Returns the value inside the `d` value.///defdestructDown(d: Down[a]): a = {let Down.Down(a) = d;a }}