From 1a4b13ceecac06843013c9fa3bc74a8a66bca37e Mon Sep 17 00:00:00 2001 From: Martin Surkovsky Date: Mon, 31 Jul 2017 22:49:09 +0200 Subject: [PATCH 1/5] ENH: support undirected graphs --- src/haydi/base/graph.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/haydi/base/graph.py b/src/haydi/base/graph.py index 4e428cb..f800725 100644 --- a/src/haydi/base/graph.py +++ b/src/haydi/base/graph.py @@ -45,11 +45,15 @@ class Graph(object): def __init__(self): self.nodes = {} + self.directed = True @property def size(self): return len(self.nodes) + def set_directed(self, directed): + self.directed = directed + def has_node(self, key): return key, self.nodes @@ -78,7 +82,8 @@ def write(self, filename): f.write(dot) def make_dot(self, name): - stream = ["digraph " + name + " {\n"] + g_type, e_type = ("digraph", "->") if self.directed else ("graph", "--") + stream = ["{} name {{\n".format(g_type)] for node in self.nodes.values(): extra = "" if node.color is not None: @@ -88,8 +93,8 @@ def make_dot(self, name): stream.append("v{} [label=\"{}\" shape=\"{}\"{}]\n".format( id(node), node.label, node.shape, extra)) for arc in node.arcs: - stream.append("v{} -> v{} [label=\"{}\"]\n".format( - id(node), id(arc.node), str(arc.data))) + stream.append("v{0} {3} v{1} [label=\"{2}\"]\n".format( + id(node), id(arc.node), str(arc.data), e_type)) stream.append("}\n") return "".join(stream) From 8e2a42aa16f2e9a35509bc50121e498839830a23 Mon Sep 17 00:00:00 2001 From: Martin Surkovsky Date: Tue, 1 Aug 2017 23:55:47 +0200 Subject: [PATCH 2/5] ENH: define iterators for basictypes Map and Set --- src/haydi/base/basictypes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/haydi/base/basictypes.py b/src/haydi/base/basictypes.py index 74f77c1..4610618 100644 --- a/src/haydi/base/basictypes.py +++ b/src/haydi/base/basictypes.py @@ -42,6 +42,9 @@ def __ne__(self, other): def __hash__(self): return hash(self.items) + def __iter__(self): + return iter(self.items) + def __repr__(self): return "{{{}}}".format(", ".join(repr(i) for i in self.items)) @@ -76,6 +79,9 @@ def __ne__(self, other): def __hash__(self): return hash(self.items) + def __iter__(self): + return iter(self.items) + def __repr__(self): r = [] for k, v in self.items: From 6fa0833fa872db225bf22aa5da35984f4b34868e Mon Sep 17 00:00:00 2001 From: Martin Surkovsky Date: Thu, 20 Jul 2017 22:22:45 +0200 Subject: [PATCH 3/5] ENH: automatic graph construction --- src/haydi/ext/graphrenderer.py | 168 +++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/haydi/ext/graphrenderer.py diff --git a/src/haydi/ext/graphrenderer.py b/src/haydi/ext/graphrenderer.py new file mode 100644 index 0000000..6ef1055 --- /dev/null +++ b/src/haydi/ext/graphrenderer.py @@ -0,0 +1,168 @@ +from collections import Iterable +from haydi.base.graph import Graph +from haydi.base.exception import HaydiException +from haydi.base.basictypes import Atom + + +class GraphRenderer(object): + + def __init__(self, graph_structure): + if not isinstance(graph_structure, Iterable): + raise Exception("Invalid graph structure." + " It is expected a list of edges.") + + self.graph_structure = tuple(graph_structure) + + self._nodes = None + self._edges = None + + def nodes(self, nodes, labels=None): + """Set the set of nodes. All graphs are consisted from the same + collection of nodes. + """ + if labels is None: + labels = get_default_node_labels(nodes) + self._node_labels = labels + + self._nodes = nodes + return self + + def assemble_graph(self, labeling_edge_fn=None): + """Tries to assemble haydi.base.graph.Graph from given structure.""" + + def assemble_edge(e_structure, vertices, labeling_edge_fn): + nodes = [] + data = [] + + # separate edge vertices from data info + for elem in e_structure: + if elem in vertices: + nodes.append(elem) + else: + data.append(elem) + + if len(nodes) != 2: + raise Exception( + "The edge consists of more or less than two vertices." + " It is expected that each edge consists of two vertices.") + + # format data values into edge label + if labeling_edge_fn is None: + edge_label = ", ".join(map(str, data)) + else: + edge_label = labeling_edge_fn(tuple(data)) + + # couple of nodes and string describing edge label + return (tuple(nodes), edge_label) + + + if self._nodes is None: # attempt to automatically identify + # nodes if not already specified + nodes, labels = self._identify_nodes(self.graph_structure) + + if not nodes: + raise Exception( + "Unknown nodes. Automatic identification failed." + " Please specify nodes manually.") + + self._nodes = nodes + self._node_labels = labels + + g = Graph() + + # parse mappings in format `{from node : to node/[to nodes]}` + if isinstance(self.graph_structure, dict): # graphs as mappings; ORIENTED + if not all(k in self._nodes in self.graph_structure.keys()): + raise Exception( + "There are nodes in structure out of" + " specified set of nodes.") + + for from_node, to_nodes in self.graph_structure.iteritems(): + if isinstance(to_nodes, dict): + raise Exception("Unknown format of edge.") + + n1 = g.node(from_node) + n1.label = self._node_labels[from_node] + + if not isinstance(to_nodes, Iterable): + to_nodes = (to_nodes,) + + for n in to_nodes: + n2 = g.node(n) + n2.label = self._node_labels[n] + n1.add_arc(n1) # TODO, solve data; how to specify data within dict; tuple, list of tuples + + else: # collection of edges; (NOT-)ORIENTED + directed = True + for e in self.graph_structure: + (n1, n2), data = assemble_edge(e, self._nodes, labeling_edge_fn) + v1 = g.node(n1) + v1.label = self._node_labels[n1] + v2 = g.node(n2) + v2.label = self._node_labels[n2] + + v1.add_arc(v2, data) + if isinstance(e, set): + directed = False + g.set_directed(directed) + return g + + def _identify_nodes(self, graph_structure): + """ + CAUTION: Automatic identification of nodes take the information about + them from edges. If there are nodes not connected by any edges, + then such nodes are not included and user have to specify the + set of nodes manually. + """ + # NOTE: is supposed that all nodes are of the same type + + nodes = set() + + # == PSEUDO CODE == + # if graph_structure represents dictionary then: + # 1. use keys as nodes # starting nodes + # 2. add nodes that are only the ends of edges # ending node + # else: + + # for each element of graph_structure: + # 1. count the number of different types in structure describing edge + # 2. pick the types that appears exactly twice (edge vertices) + # 2.1. if there are more types with exactly two appearances: + # FAIL; can't guess + # 2.2. else: + # place these elements among nodes + + # == IMPLEMENTATION == + if isinstance(graph_structure, dict): + nodes = nodes.union(graph_structure.keys()) + + for elem in graph_structure.itervalues(): + if isinstance(elem, Iterable): + nodes = nodes.union(elem) + else: + nodes.add(elem) + else: + if graph_structure: + # NOTE: each edge has to consists of two nodes at least + sample_edge = graph_structure[0] + + counts = {v: 0 for v in set(map(type, sample_edge))} + for elem in sample_edge: + counts[type(elem)] += 1 + + relevant = filter(lambda (t, c): c == 2, counts.iteritems()) + if len(relevant) != 1: + raise Exception( + "Unable to identify nodes from the given structure." + " Please specify nodes manually.") + + node_t, _ = relevant.pop() + for edge in graph_structure: + nodes = nodes.union(elem for elem in edge + if isinstance(elem, node_t)) + + return (nodes, get_default_node_labels(nodes)) + + +def get_default_node_labels(nodes): + return {node: str(node) for node in nodes} From b581c2495fba8ca0511343f3f7a2ab0b3b9a8c28 Mon Sep 17 00:00:00 2001 From: Martin Surkovsky Date: Thu, 3 Aug 2017 14:21:29 +0200 Subject: [PATCH 4/5] FIX: firstly fill nodes before adding edges --- src/haydi/ext/graphrenderer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/haydi/ext/graphrenderer.py b/src/haydi/ext/graphrenderer.py index 6ef1055..bb41950 100644 --- a/src/haydi/ext/graphrenderer.py +++ b/src/haydi/ext/graphrenderer.py @@ -70,6 +70,10 @@ def assemble_edge(e_structure, vertices, labeling_edge_fn): g = Graph() + for node in self._nodes: # add all nodes into graph + n = g.node(node) + n.label = self._node_labels[node] + # parse mappings in format `{from node : to node/[to nodes]}` if isinstance(self.graph_structure, dict): # graphs as mappings; ORIENTED if not all(k in self._nodes in self.graph_structure.keys()): @@ -82,14 +86,12 @@ def assemble_edge(e_structure, vertices, labeling_edge_fn): raise Exception("Unknown format of edge.") n1 = g.node(from_node) - n1.label = self._node_labels[from_node] if not isinstance(to_nodes, Iterable): to_nodes = (to_nodes,) for n in to_nodes: n2 = g.node(n) - n2.label = self._node_labels[n] n1.add_arc(n1) # TODO, solve data; how to specify data within dict; tuple, list of tuples else: # collection of edges; (NOT-)ORIENTED From c432fe8559c54c9140e9cd9ddad529af60c0d383 Mon Sep 17 00:00:00 2001 From: Martin Surkovsky Date: Thu, 3 Aug 2017 14:22:53 +0200 Subject: [PATCH 5/5] TEST: graphrenderer It serves also to present how to work with graph renderer. --- tests/test_graphrenderer.py | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_graphrenderer.py diff --git a/tests/test_graphrenderer.py b/tests/test_graphrenderer.py new file mode 100644 index 0000000..b208c08 --- /dev/null +++ b/tests/test_graphrenderer.py @@ -0,0 +1,77 @@ +from testutils import init +init() + +from haydi.ext.graphrenderer import GraphRenderer +import haydi as hd + +import itertools + +def test_make_oriented_graph(): + + # TEST 1 =================================================================== + nodes = hd.ASet(2, "n") + n0, n1 = nodes.run() + + graphs = hd.Subsets(nodes * nodes) + + g_structure = graphs.filter(lambda gs: len(gs) > 2).first().run() + # g_structure = {(n0, n0), (n0, n1), (n1, n0)} + + gr = GraphRenderer(g_structure) + g = gr.nodes(nodes, + {n: "A({})".format(i) + for i, n in enumerate(nodes)}) \ + .assemble_graph() + assert g is not None + a0, a0_presented = g.node_check(n0) + assert a0_presented + a1, a1_presented = g.node_check(n1) + assert a1_presented + + assert len(a0.arcs) == 2 + a0_targets = map(lambda a: a.node, a0.arcs) + assert a0 in a0_targets and a1 in a0_targets + + # TEST 2 =================================================================== + nodes = (0, 1, 2, 3, 4) + g_structure = {(1, 2, 2.3), + (2,3, 1.4), + (1,3, 3.4), + (3, 3, 6)} # the '6' make automatic nodes identification impossible, + # the same type, but when the exact nodes are specified, + # it's OK. + gr = GraphRenderer(g_structure) + try: + # assembling without specifying nodes throws exception in this case + g = gr.assemble_graph() + except Exception: + assert True + + # -------------------------------------------------------------------------- + + g = gr \ + .nodes(nodes) \ + .assemble_graph() + + n0= g.node(0) + assert len(n0.arcs) == 0 + + +def test_make_unoriented_graph(): + + # hasse diagram of powerset above three elements + s = (0, 1, 2) + powerset = list(itertools.chain.from_iterable(itertools.combinations(s, r) + for r in range(len(s)+1))) + edges = ({a, b} for a, b in itertools.product(powerset, powerset) + if a != b and # different elements + set(a).issubset(b) and # subset relation + len(set(b).difference(a)) == 1) # exclude transitive edges + + + gr = GraphRenderer(edges) + g = gr \ + .nodes(powerset, + {n: "{" + ", ".join(map(str, n)) + "}" for n in powerset}) \ + .assemble_graph() + assert not g.directed