meshed.itools#
Graph operations over adjacency mappings.
Here a graph g is any Mapping whose keys are nodes and whose values
are iterables of the nodes they point to (g[src] lists the dst nodes of
the edges src -> dst). A plain dict of lists is the usual form, but any
Mapping with iterable values works, including strings, where each character is
a node. Nodes that only appear as destinations need not be keys. The functions
here mostly iterate or compute sets over such a mapping without building any
other graph structure; meshed.dag uses them to order and query its
FuncNode graph.
Main entry points:
topological_sort: order the nodes so that every node comes after its parents.edgesandnodes: iterate the edges or the (deduplicated) nodes ofg.root_nodesandleaf_nodes: nodes with no parents, or no children.ancestorsanddescendants: everything reachable to, or from, some nodes.edge_reversed_graph: the same graph with every edge flipped.
>>> from meshed.itools import topological_sort, root_nodes, leaf_nodes
>>> g = {0: [1, 2], 1: [3], 2: [3]}
>>> topological_sort(g)
[0, 1, 2, 3]
>>> root_nodes(g), leaf_nodes(g)
({0}, {3})
Functions
|
Add an edge FROM node1 TO node2 |
|
Set of all nodes (not in source) reachable TO |
|
Set of all nodes (not in source) adjacent FROM 'source' in 'g' |
|
Shallow copy of |
|
Returns the set of all nodes reachable FROM |
|
Invert the from/to direction of the edges of the graph. |
|
Generates edges of graph, i.e. |
|
Keep the |
|
Keep, in each value of |
|
find a path from src to dst nodes in graph |
Makes a graphviz graph using the links specified by dict d |
|
|
Returns a list representing a cycle in the graph if any. An empty list indicates no cycle. |
|
Returns True if the graph has given node |
|
Yield |
Nodes of |
|
|
Nodes of |
|
Yield every node of |
|
Set of the keys of |
|
Yield |
|
Set of all nodes (not in source) adjacent TO 'source' in 'g' |
|
Iterator of nodes that have directed paths TO node |
|
Get a random graph. |
Generator of reversed edges. |
|
|
Returns the roots of the sub-dag that contribute to compute the given nodes. |
|
Nodes of |
|
Copy of |
|
Iterator of nodes that have directed paths FROM node |
Return the list of nodes in topological sort order. |
- meshed.itools.ancestors(g, source, _exclude_nodes=None)[source]#
Set of all nodes (not in source) reachable TO
sourceing.>>> g = { ... 0: [1, 2], ... 1: [2, 3, 4], ... 2: [4], ... 3: [4] ... } >>> ancestors(g, [2, 3]) {0, 1} >>> ancestors(g, [0]) set()
- meshed.itools.children(g, source)[source]#
Set of all nodes (not in source) adjacent FROM ‘source’ in ‘g’
>>> g = { ... 0: [1, 2], ... 1: [2, 3, 4], ... 2: [1, 4], ... 3: [4] ... } >>> children(g, [2, 3]) {1, 4} >>> children(g, [4]) set()
- meshed.itools.copy_of_g_with_some_keys_removed(g, keys)[source]#
Shallow copy of
gwithout the given keys.A whitespace-separated string of keys is accepted. References to the removed keys inside other adjacencies are kept.
- meshed.itools.descendants(g, source, _exclude_nodes=None)[source]#
Returns the set of all nodes reachable FROM
sourceing.>>> g = { ... 0: [1, 2], ... 1: [2, 3, 4], ... 2: [4], ... 3: [4] ... } >>> descendants(g, [2, 3]) {4} >>> descendants(g, [4]) set()
- meshed.itools.edge_reversed_graph(g, dst_nodes_factory=<class 'list'>, dst_nodes_append=<method 'append' of 'list' objects>)[source]#
Invert the from/to direction of the edges of the graph.
>>> g = dict(a='c', b='cd', c='abd', e='') >>> assert edge_reversed_graph(g) == { ... 'c': ['a', 'b'], 'd': ['b', 'c'], 'a': ['c'], 'b': ['c'], 'e': []} >>> reverse_g_with_sets = edge_reversed_graph(g, set, set.add) >>> assert reverse_g_with_sets == { ... 'c': {'a', 'b'}, 'd': {'b', 'c'}, 'a': {'c'}, 'b': {'c'}, 'e': set([])}
Testing border cases
>>> assert edge_reversed_graph(dict(e='', a='e')) == {'e': ['a'], 'a': []} >>> assert edge_reversed_graph(dict(a='e', e='')) == {'e': ['a'], 'a': []}
- meshed.itools.edges(g)[source]#
Generates edges of graph, i.e.
(from_node, to_node)tuples.>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> assert sorted(edges(g)) == [ ... ('a', 'c'), ('b', 'c'), ('b', 'e'), ('c', 'a'), ('c', 'b'), ('c', 'd'), ... ('c', 'e'), ('d', 'c'), ('e', 'c'), ('e', 'z')]
- meshed.itools.filter_dict_on_keys(d, condition)[source]#
Keep the
(k, v)items ofdfor whichcondition(k, v)is true.
- meshed.itools.filter_dict_with_list_values(d, condition)[source]#
Keep, in each value of
d, only the elements satisfyingcondition.The filtered values are lists, whatever the originals were.
- meshed.itools.find_path(g, src, dst, path=None)[source]#
find a path from src to dst nodes in graph
>>> g = dict(a='c', b='ce', c=list('abde'), d='c', e=['c', 'z'], f={}) >>> find_path(g, 'a', 'c') ['a', 'c'] >>> find_path(g, 'a', 'b') ['a', 'c', 'b'] >>> find_path(g, 'a', 'z') ['a', 'c', 'b', 'e', 'z'] >>> assert find_path(g, 'a', 'f') == None
- meshed.itools.graphviz_digraph(d)[source]#
Makes a graphviz graph using the links specified by dict d
- meshed.itools.has_cycle(g)[source]#
Returns a list representing a cycle in the graph if any. An empty list indicates no cycle.
- Parameters:
g (
Mapping[TypeVar(N),Iterable[TypeVar(N)]]) –- The graph to check for cycles, represented as a dictionary where keys are nodes
and values are lists of nodes pointing to the key node (parents of the key node).
Example usage:
>>> g = dict(e=['c', 'd'], c=['b'], d=['b'], b=['a']) >>> has_cycle(g) []
>>> g['a'] = ['e'] # Introducing a cycle >>> has_cycle(g) ['e', 'c', 'b', 'a', 'e']
- Return type:
Design notes:
Graph Representation: The graph is interpreted such that each key is a child node, and the values are lists of its parents. This representation requires traversing the graph in reverse, from child to parent, to detect cycles.
I regret this design choice, which was aligned with the original problem that was being solved, but which doesn’t follow the usual representation of a graph.
Consistent Return Type: The function systematically returns a list. A non-empty list indicates a cycle (showing the path of the cycle), while an empty list indicates the absence of a cycle.
Depth-First Search (DFS): The function performs a DFS on the graph to detect cycles. It uses a recursion stack (rec_stack) to track the path being explored and a visited set (visited) to avoid re-exploring nodes.
Cycle Detection and Path Reconstruction: When a node currently in the recursion stack is encountered again, a cycle is detected. The function then reconstructs the cycle path from the current path explored, including the start and end node to illustrate the cycle closure.
Efficient Backtracking: After exploring a node’s children, the function backtracks by removing the node from the recursion stack and the current path, ensuring accurate path tracking for subsequent explorations.
- meshed.itools.has_node(g, node, check_adjacencies=True)[source]#
Returns True if the graph has given node
>>> g = { ... 0: [1, 2], ... 1: [2] ... } >>> has_node(g, 0) True >>> has_node(g, 2) True
Note that 2 was found, though it’s not a key of
g. This shows that we don’t have to have an explicit{2: []}ingto be able to see that it’s a node ofg. The function will go through the values of the mapping to try to find it if it hasn’t been found before in the keys.This can be inefficient, so if that matters, you can express your graph
gso that all nodes are explicitly declared as keys, and usecheck_adjacencies=Falseto tell the function not to look into the values of thegmapping.>>> has_node(g, 2, check_adjacencies=False) False >>> g = { ... 0: [1, 2], ... 1: [2], ... 2: [] ... } >>> has_node(g, 2, check_adjacencies=False) True
- meshed.itools.in_degrees(g)[source]#
Yield
(node, number_of_parents)for every node ofg.>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> assert dict(in_degrees(g)) == ( ... {'a': 1, 'b': 1, 'c': 4, 'd': 1, 'e': 2, 'f': 0, 'z': 1} ... )
- meshed.itools.isolated_nodes(g)[source]#
Nodes of
gwhose adjacency is empty (no outgoing edges).>>> g = dict(a='c', b='ce', c=list('abde'), d='c', e=['c', 'z'], f={}) >>> set(isolated_nodes(g)) {'f'}
- meshed.itools.leaf_nodes(g)[source]#
Nodes of
gthat point to no other node (isolated nodes included).>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> sorted(leaf_nodes(g)) ['f', 'z']
Note that
fis present: Isolated nodes are considered both as root and leaf nodes both.
- meshed.itools.nodes(g)[source]#
Yield every node of
gonce: each key, then each node it points to.>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> sorted(nodes(g)) ['a', 'b', 'c', 'd', 'e', 'f', 'z']
- meshed.itools.nodes_of_graph(graph)[source]#
Set of the keys of
graphtogether with its values taken whole.The values go in as they are, so they must be hashable.
- meshed.itools.out_degrees(g)[source]#
Yield
(node, number_of_children)for every key ofg.>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> assert dict(out_degrees(g)) == ( ... {'a': 1, 'b': 2, 'c': 4, 'd': 1, 'e': 2, 'f': 0} ... )
- meshed.itools.parents(g, source)[source]#
Set of all nodes (not in source) adjacent TO ‘source’ in ‘g’
>>> g = { ... 0: [1, 2], ... 1: [2, 3, 4], ... 2: [1, 4], ... 3: [4] ... } >>> parents(g, [2, 3]) {0, 1} >>> parents(g, [0]) set()
- meshed.itools.predecessors(g, node)[source]#
Iterator of nodes that have directed paths TO node
>>> g = { ... 0: [1, 2], ... 1: [2, 3, 4], ... 2: [1, 4], ... 3: [4]} >>> set(predecessors(g, 4)) {0, 1, 2, 3} >>> set(predecessors(g, 2)) {0, 1, 2} >>> set(predecessors(g, 0)) set()
Notice that 2 is a predecessor of 2 here because of the presence of a 2-1-2 directed path.
- meshed.itools.random_graph(n_nodes=7)[source]#
Get a random graph.
>>> random_graph() {0: [6, 3, 5, 2], 1: [3, 2, 0, 6], 2: [5, 6, 4, 0], 3: [1, 0, 5, 6, 3], 4: [], 5: [1, 5, 3, 6], 6: [4, 3, 1]} >>> random_graph(3) {0: [0], 1: [0], 2: []}
- meshed.itools.reverse_edges(g)[source]#
Generator of reversed edges. Like edges but with inverted edges.
>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> assert sorted(reverse_edges(g)) == [ ... ('a', 'c'), ('b', 'c'), ('c', 'a'), ('c', 'b'), ('c', 'd'), ('c', 'e'), ... ('d', 'c'), ('e', 'b'), ('e', 'c'), ('z', 'e')]
Note
Not to be confused with
edge_reversed_graphwhich inverts the direction of edges.
- meshed.itools.root_ancestors(graph, nodes)[source]#
Returns the roots of the sub-dag that contribute to compute the given nodes.
- meshed.itools.root_nodes(g)[source]#
Nodes of
gthat no other node points to (isolated nodes included).>>> g = dict(a='c', b='ce', c='abde', d='c', e=['c', 'z'], f={}) >>> sorted(root_nodes(g)) ['f']
Note that
fis present: Isolated nodes are considered both as root and leaf nodes both.
- meshed.itools.subtract_subgraph(graph, subgraph)[source]#
Copy of
graphwith the nodes ofsubgraphremoved.The nodes are those of
nodes_of_graph(subgraph); they are removed from keys and adjacencies, and keys left with no adjacencies are dropped.
- meshed.itools.successors(g, node, _exclude_nodes=None)[source]#
Iterator of nodes that have directed paths FROM node
>>> g = { ... 0: [1, 2], ... 1: [2, 3, 4], ... 2: [1, 4], ... 3: [4]} >>> assert set(successors(g, 1)) == {1, 2, 3, 4} >>> assert set(successors(g, 3)) == {4} >>> assert set(successors(g, 4)) == set()
Notice that 1 is a successor of 1 here because there’s a 1-2-1 directed path
- meshed.itools.topological_sort(g)[source]#
Return the list of nodes in topological sort order.
This order is such that a node’s parents will all occur before it: if
order[i]is a parent oforder[j]theni < j.This is often used to compute the order of computation in a DAG.
>>> g = { ... 0: [4, 2], ... 4: [3, 1], ... 2: [3], ... 3: [1] ... } >>> >>> list(topological_sort(g)) [0, 4, 2, 3, 1]
Here’s an ascii art of the graph, to verify that the topological sort is indeed as expected.
┌───┐ ┌───┐ ┌───┐ ┌───┐ │ 0 │ ──▶ │ 2 │ ──▶ │ 3 │ ──▶ │ 1 │ └───┘ └───┘ └───┘ └───┘ │ ▲ ▲ │ │ │ ▼ │ │ ┌───┐ │ │ │ 4 │ ────────────────┼─────────┘ └───┘ │ │ │ └───────────────────┘