Skip to content

Commit 0f9d939

Browse files
committed
update main routine to run evo algo.
1 parent 2ffb161 commit 0f9d939

8 files changed

+591
-11
lines changed

.gitignore

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
graph_gen.py
21
**.pyc*
3-
*.idea/
2+
*.idea/
3+
.coverage
4+
dist/
5+
graph_stitcher.egg-info/

README.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ As mentioned above graph stitcher is pluggable, to test different algorithms of
5555
interface for the *stitch()* and *validate()* routine (see *BaseStitcher*
5656
class).
5757

58-
To stitch two graphs the tool needs to know how which relationships are needed:
58+
To stitch two graphs the tool needs to know which relationships are needed:
5959

6060
Type of source node | Type of target node
6161
-----------------------------------------
@@ -97,7 +97,7 @@ The following dictionary can be passed in as a composition condition:
9797
('share', ('group', ['x', 'y']))]
9898
}
9999

100-
graph stitcher is mostly developed to test & play around. Also to check if
100+
This graph stitcher is mostly developed to test & play around. Also to check if
101101
[evolutionary algorithms](https://en.wikipedia.org/wiki/Evolutionary_algorithm)
102102
can be developed to determine the best resulting graph.
103103

@@ -114,3 +114,12 @@ dotted lines. Each graph is a candidate solution, the results of the
114114
validation are shown as titles of the graphs.
115115

116116
![output](./figure_1.png?raw=true "Output")
117+
118+
To test the evolutionary algorithm run:
119+
120+
$ ./run_me.py -ea
121+
122+
Please note that it might not always find a set of good solutions, as the
123+
container and the request are pretty small. Also note that currently the
124+
fitness function expresses a fitness for the given conditions; and does not
125+
include a fitness value for the validation phase.

build.sh

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
3+
pep8 -r **/*.py
4+
5+
pylint -r n **/*.py
6+
7+
nosetests --with-coverage --cover-package=stitcher

run_me.py

+50-5
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,77 @@
55
"""
66

77
import json
8+
import random
9+
import sys
10+
import logging
11+
import networkx as nx
812

913
from networkx.readwrite import json_graph
1014

15+
from stitcher import evolutionary
1116
from stitcher import stitch
1217
from stitcher import vis
1318

19+
FORMAT = "%(asctime)s - %(filename)s - %(lineno)s - " \
20+
"%(levelname)s - %(message)s"
21+
logging.basicConfig(format=FORMAT, level=logging.DEBUG)
1422

15-
def main():
23+
24+
def main(use_evol=False):
1625
"""
1726
main routine.
1827
"""
1928
container_tmp = json.load(open('data/container.json'))
2029
container = json_graph.node_link_graph(container_tmp, directed=True)
2130
request_tmp = json.load(open('data/request.json'))
2231
request = json_graph.node_link_graph(request_tmp, directed=True)
32+
stitches = json.load(open('data/stitch.json'))
2333

2434
# XXX: change this to whatever stitcher you want to use
2535
stitcher = stitch.IncomingEdgeStitcher()
26-
# stitcher = stitch.NodeRankStitcher()
27-
graphs = stitcher.stitch(container, request)
36+
37+
if use_evol:
38+
evo = evolutionary.BasicEvolution(percent_cutoff=0.9,
39+
percent_mutate=0.0)
40+
41+
condy = {'compositions': {('diff', ('k', 'l'))}}
42+
43+
# initial population
44+
population = []
45+
for _ in range(10):
46+
tmp = {}
47+
for item in request.nodes():
48+
trg_cand = random.choice(list(['A', 'B', 'C', 'D', 'E', 'F']))
49+
tmp[item] = trg_cand
50+
population.append(evolutionary.GraphCandidate(tmp, stitches,
51+
condy, [], request,
52+
container))
53+
54+
_, population = evo.run(population,
55+
10, # do 10 steps
56+
fitness_goal=-1.0) # want multiple
57+
58+
if population[0].fitness() != 0.0:
59+
print('Please rerun - did not find a viable solution')
60+
sys.exit(1)
61+
62+
graphs = []
63+
for candidate in population:
64+
if candidate.fitness() == 0.0:
65+
tmp_graph = nx.union(container, request)
66+
for item in candidate.gen:
67+
tmp_graph.add_edge(item, candidate.gen[item])
68+
graphs.append(tmp_graph)
69+
else:
70+
graphs = stitcher.stitch(container, request)
2871
results = stitcher.validate(graphs, {'b': 5})
29-
# results = stitcher.validate(graphs, {'a': (1, 4)})
3072

3173
# XXX: disable this if you do not want to see the results.
3274
vis.show(graphs, request.nodes(), results)
3375

3476

3577
if __name__ == '__main__':
36-
main()
78+
USE_EA = False
79+
if len(sys.argv) == 2 and sys.argv[1] == '-ea':
80+
USE_EA = True
81+
main(USE_EA)

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from setuptools import setup
22

33
setup(name='graph_stitcher',
4-
version='0.0.9',
4+
version='0.0.10',
55
author='Thijs Metsch',
66
url='https://github.com/tmetsch/graph_stitcher',
77
description=('This tool is a little framework to determine possible'

stitcher/evolutionary.py

+216
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,149 @@
55

66
import logging
77
import random
8+
import re
89

910
LOG = logging.getLogger()
1011

1112

13+
def _eq_attr(node, attr, gens, container):
14+
"""
15+
Calcs fitness based on the fact that node need target node to have an attr
16+
with a certain value.
17+
"""
18+
trg_nd = container.node[gens[node]]
19+
if attr[0] not in trg_nd:
20+
return 10.1
21+
elif attr[1] != trg_nd[attr[0]]:
22+
return 10.2
23+
return 0.0
24+
25+
26+
def _neq_attr(node, attr, gens, container):
27+
"""
28+
Calcs fitness based on the fact that node's target shall not have an attr
29+
with a certain value.
30+
"""
31+
trg_nd = container.node[gens[node]]
32+
if attr[0] in trg_nd and attr[1] == trg_nd[attr[0]]:
33+
return 10.1
34+
return 0.0
35+
36+
37+
def _lg_attr(node, attr, gens, container):
38+
"""
39+
Calcs fitness based on the fact that node's target node shall have an attr
40+
with a value larger than the given one.
41+
"""
42+
trg_nd = container.node[gens[node]]
43+
if attr[0] not in trg_nd:
44+
return 10.1
45+
elif attr[1] > trg_nd[attr[0]]:
46+
return 10.2
47+
return 0.0
48+
49+
50+
def _lt_attr(node, attr, gens, container):
51+
"""
52+
Calcs fitness based on the fact that node's target node shall have an attr
53+
with a value smaller than the given one.
54+
"""
55+
trg_nd = container.node[gens[node]]
56+
if attr[0] not in trg_nd:
57+
return 10.1
58+
elif attr[1] < trg_nd[attr[0]]:
59+
return 10.2
60+
return 0.0
61+
62+
63+
def _regex_attr(node, attr, gens, container):
64+
"""
65+
Calcs fitness based on the fact that node's target node shall have an attr
66+
with a value smaller than the given one.
67+
"""
68+
trg_nd = container.node[gens[node]]
69+
if attr[0] not in trg_nd:
70+
return 10.1
71+
elif not re.search(attr[1], trg_nd[attr[0]]):
72+
return 10.2
73+
return 0.0
74+
75+
76+
def _same_target(node1, node2, gens):
77+
"""
78+
Calcs fitness based on the fact that two nodes should share same target.
79+
"""
80+
shared_tg = None
81+
for src in gens:
82+
if shared_tg is None and src in [node1, node2]:
83+
shared_tg = gens[src]
84+
elif shared_tg is not None and gens[src] != shared_tg:
85+
return 10.0
86+
return 0.0
87+
88+
89+
def _diff_target(node1, node2, gens):
90+
"""
91+
Calc fitness based on the fact that two nodes should not share same target.
92+
"""
93+
shared_tg = None
94+
for src in gens:
95+
if shared_tg is None and src in [node1, node2]:
96+
shared_tg = gens[src]
97+
elif shared_tg is not None and gens[src] == shared_tg:
98+
return 10.0
99+
return 0.0
100+
101+
102+
def _share_attr(attrn, node_list, gens, container):
103+
"""
104+
Calcs fitness based on the fact that two nodes from the request should be
105+
stitched to two nodes in the container two share the same attribute
106+
"""
107+
attrv = None
108+
for node in node_list:
109+
trg = gens[node]
110+
if attrn not in container.node[trg]:
111+
return 10.1
112+
elif attrv is None:
113+
attrv = container.node[trg][attrn]
114+
elif attrv != container.node[trg][attrn]:
115+
return 10.2
116+
return 0.0
117+
118+
119+
def _my_filter(conditions, gens, container):
120+
"""
121+
Apply filters.
122+
"""
123+
res = 0.0
124+
if 'attributes' in conditions:
125+
for condition in conditions['attributes']:
126+
para1 = condition[1][0]
127+
para2 = condition[1][1]
128+
if condition[0] == 'eq':
129+
res += _eq_attr(para1, para2, gens, container)
130+
if condition[0] == 'neq':
131+
res += _neq_attr(para1, para2, gens, container)
132+
if condition[0] == 'lg':
133+
res += _lg_attr(para1, para2, gens, container)
134+
if condition[0] == 'lt':
135+
res += _lt_attr(para1, para2, gens, container)
136+
if condition[0] == 'regex':
137+
res += _regex_attr(para1, para2, gens, container)
138+
if 'compositions' in conditions:
139+
for condition in conditions['compositions']:
140+
para1 = condition[1][0]
141+
para2 = condition[1][1]
142+
if condition[0] == 'same':
143+
res += _same_target(para1, para2, gens)
144+
if condition[0] == 'diff':
145+
res += _diff_target(para1, para2, gens)
146+
if condition[0] == 'share':
147+
res += _share_attr(para1, para2, gens, container)
148+
return res
149+
150+
12151
class Candidate(object):
13152
"""
14153
A candidate of a population for an evolutionary algorithm
@@ -41,6 +180,83 @@ def crossover(self, partner):
41180
"""
42181
raise NotImplementedError('Not done yet.')
43182

183+
def __eq__(self, other):
184+
return self.gen == other.gen
185+
186+
187+
class GraphCandidate(Candidate):
188+
"""
189+
Candidate within a population. The DNA of this candidate is defined by a
190+
dictionary of source to target stitches.
191+
"""
192+
193+
def __init__(self, gen, stitch, conditions, mutation_list, request,
194+
container):
195+
super(GraphCandidate, self).__init__(gen)
196+
self.stitch = stitch
197+
self.conditions = conditions
198+
self.mutation_list = mutation_list
199+
self.request = request
200+
self.container = container
201+
202+
def fitness(self):
203+
fit = 0.0
204+
205+
# 1. stitch
206+
for src in self.gen:
207+
trg = self.gen[src]
208+
if self.container.node[trg]['type'] != \
209+
self.stitch[self.request.node[src]['type']]:
210+
fit += 100
211+
212+
# 2. conditions
213+
fit += _my_filter(self.conditions, self.gen, self.container)
214+
215+
return fit
216+
217+
def mutate(self):
218+
# let's mutate to an option outside of the shortlisted candidate list.
219+
src = random.choice(list(self.gen.keys()))
220+
221+
done = False
222+
cutoff = len(self.gen)
223+
i = 0
224+
while not done and i <= cutoff:
225+
# break off as there might be no other match available.
226+
nd_trg = self.mutation_list[random.randint(
227+
0, len(self.mutation_list) - 1)][0]
228+
if self.container.node[nd_trg]['type'] == \
229+
self.stitch[self.request.node[src]['type']]:
230+
done = True
231+
self.gen[src] = nd_trg
232+
i += 1
233+
234+
def crossover(self, partner):
235+
tmp = {}
236+
237+
for src in self.gen:
238+
# pick one from partner
239+
done = False
240+
nd_trg = ''
241+
cutoff = len(self.gen)
242+
i = 0
243+
while not done and i <= cutoff:
244+
nd_trg = random.choice(list(partner.gen.values()))
245+
if self.container.node[nd_trg]['type'] \
246+
== self.stitch[self.request.node[src]['type']]:
247+
done = True
248+
i += 1
249+
if done:
250+
tmp[src] = nd_trg
251+
else:
252+
tmp[src] = self.gen[src]
253+
254+
return self.__class__(tmp, self.stitch, self.conditions,
255+
self.mutation_list, self.request, self.container)
256+
257+
def __repr__(self):
258+
return 'f: ' + str(self.fitness()) + ' - ' + repr(self.gen)
259+
44260

45261
class BasicEvolution(object):
46262
"""

0 commit comments

Comments
 (0)