Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

automatic graph construction #27

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/haydi/base/basictypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions src/haydi/base/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down
170 changes: 170 additions & 0 deletions src/haydi/ext/graphrenderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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()

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()):
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)

if not isinstance(to_nodes, Iterable):
to_nodes = (to_nodes,)

for n in to_nodes:
n2 = g.node(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}
77 changes: 77 additions & 0 deletions tests/test_graphrenderer.py
Original file line number Diff line number Diff line change
@@ -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