def test_label_fst(): '''Label small FSTs and check results''' O = (0, 0) # dummy location Y = SteinerTree('abcs', ['as', 'bs', 'cs'], {'a': O, 'b': O, 'c': O}) assert label_fst(Y) == ('b', 'c') H = SteinerTree('abcdst', ['as', 'bs', 'st', 'tc', 'td'], { 'a': O, 'b': O, 'c': O, 'd': O }) assert label_fst(H) == ('b', ('c', 'd')) # from Fampa, figure 4 t8 = SteinerTree('123456789abcdefg', [ '1a', 'ab', 'ac', 'bd', 'b2', 'ce', 'cf', 'd3', 'd4', 'eg', 'e5', 'f6', 'f7', 'g8', 'g9' ], { '1': O, '2': O, '3': O, '4': O, '5': O, '6': O, '7': O, '8': O, '9': O }) assert label_fst(t8) == ((('2', ('3', '4')), (('5', ('8', '9')), ('6', '7'))))
def make_forward_flow(tree, flow): '''Create tree with arcs oriented for positive, forward flow. args: - tree: a SteinerTree - flow: a dict from arc to (signed) flow value returns: - a new SteinerTree - a new flow dict with only positive flow values ''' new_nodes = tree.get_nodes() new_arcs = [] new_pos = tree.get_terminal_positions() new_flow = {} for u, v in tree.get_arcs(): if flow[u, v] >= 0.0: new_arcs.append((u, v)) new_flow[u, v] = flow[u, v] else: new_arcs.append((v, u)) new_flow[v, u] = -1.0 * flow[u, v] new_tree = SteinerTree(new_nodes, new_arcs, new_pos) return new_tree, new_flow
def test_solve(): '''Solve model and check some solution properties''' tree = SteinerTree('abcs', ['as', 'bs', 'cs'], { 'a': (0, 0), 'b': (10, 0), 'c': (0, 10) }) demand = {'a': -30, 'b': 20, 'c': 10} diams = [0.4, 0.6, 0.8, 1., 1.2] costs = [680., 910., 1200., 1550., 1960.] pres = [40, 80] C = 1.0 steiner_pos = {'s': (5, 5)} flow = find_arc_flow(tree, demand) ds, ps, obj = solve(tree, flow, steiner_pos, diams, costs, pres, C) assert obj is not None for n, p in ps.items(): assert p >= min(pres) - TOL assert p <= max(pres) + TOL for a, d in ds.items(): assert d >= min(diams) - TOL assert d <= max(diams) + TOL
def test_no_steiner(term_pos): '''nothing to do for tree with no steiner nodes''' tree = SteinerTree('abc', ['ab', 'bc'], term_pos) pos = {} assert not is_degenerate(tree, pos) assert degenerate_edges(tree, pos) == [] mtree, mpos = merged(tree, pos) assert mtree == tree assert mpos == pos
def test_no_crossings(): no_edge = SteinerTree('a', [], {'a': (0, 0)}) assert crossing_edges(no_edge, {}) == [] edge = SteinerTree('ab', ['ab'], {'a': (0, 0), 'b': (0, 1)}) assert crossing_edges(edge, {}) == [] path = SteinerTree('abc', ['ab', 'bc'], { 'a': (0, 0), 'b': (0, 1), 'c': (0, 2) }) assert crossing_edges(edge, {}) == [] star = SteinerTree('abcs', ['as', 'bs', 'cs'], { 'a': (0, 0), 'b': (2, 0), 'c': (0, 2) }) assert crossing_edges(star, {'s': (1, 1)}) == []
def test_merge_pos(): term_pos = {'a': 1, 'b': 2, 'c': 3} star = SteinerTree('abcs', ['as', 'bs', 'cs'], term_pos) steiner_pos = {'s': 4} pos = merge_pos(star, steiner_pos) assert len(pos) == 4 assert pos['a'] == 1 assert pos['b'] == 2 assert pos['c'] == 3 assert pos['s'] == 4
def test_positions(): pos = {'a': 1, 'b': 2, 'c': 3} star = SteinerTree('abcs', ['as', 'bs', 'cs'], pos) assert star.get_position('a') == 1 assert star.get_position('b') == 2 assert star.get_position('c') == 3 assert star.get_terminal_positions() == pos with pytest.raises(KeyError) as e: star.get_position('s') assert 'Not a terminal' in e.value.message
def test_crossings(): pair = SteinerTree('abcd', ['ab', 'cd'], { 'a': (0, 1), 'b': (2, 1), 'c': (1, 0), 'd': (1, 2) }) edges = crossing_edges(pair, {}) assert len(edges) == 1 assert set(edges[0]) == set([('a', 'b'), ('c', 'd')]) path = SteinerTree('abcde', ['ab', 'bc', 'cd', 'de'], { 'a': (1, 0), 'b': (1, 1), 'c': (2, 0), 'e': (0, 0) }) edges = crossing_edges(path, {'d': (2, 1)}) assert len(edges) == 2 for p in edges: assert ('d', 'e') in p assert ('a', 'b') in p or ('b', 'c') in p assert edges[0] != edges[1]
def test_SteinerTree(): nodes = ['a', 'b', 'c', 's'] arcs = [('a', 's'), ('b', 's'), ('c', 's')] pos = {'a': (0, 0), 'b': (1, 0), 'c': (0, 1)} tree = SteinerTree(nodes, arcs, pos) for n in 'abc': assert tree.is_terminal(n) assert tree.is_steiner('s') assert set(tree.get_terminal_nodes()) == set('abc') assert set(tree.get_steiner_nodes()) == set(['s']) assert tree.is_full_steiner_topology()
def merged(tree, steiner_pos, abstol=abstol): '''build new tree that merges all degenerate edges. when merging an edge, the lexicographically smaller node will survive. returns a tree and a matching dict of steiner node positions. ''' degedges = degenerate_edges(tree, steiner_pos, abstol) # key: removed node, value: remaining node (taking over) turn_into = {} for u, v in degedges: if tree.is_terminal(u) and tree.is_terminal(v): # don't merge terminals continue elif tree.is_terminal(u): # keep terminals pass elif tree.is_terminal(v): # keep terminals u, v = v, u elif v < u: # keep lexicographically smaller node u, v = v, u turn_into[v] = u # merge nodes into transitive end-point for v, u in turn_into.iteritems(): while u in turn_into: u = turn_into[u] turn_into[v] = u # build new tree data new_nodes = [u for u in tree.get_nodes() if u not in turn_into] new_edges = [] for u, v in tree.get_arcs(): uu, vv = turn_into.get(u, u), turn_into.get(v, v) if uu != vv: # remove self-loops new_edges.append((uu, vv)) new_tree = SteinerTree(new_nodes, new_edges, tree.get_terminal_positions()) new_pos = {s: steiner_pos[s] for s in steiner_pos if s in new_nodes} return new_tree, new_pos
def test_merged_multi(): tree = SteinerTree('abcdst', ['as', 'bs', 'st', 'tc', 'td'], { 'a': (0, 0), 'b': (0, 2), 'c': (2, 2), 'd': (2, 0) }) pos = {'s': (0, 0), 't': (0, 0)} assert is_degenerate(tree, pos) assert set(degenerate_edges(tree, pos)) == set([('a', 's'), ('s', 't')]) mtree, mpos = merged(tree, pos) assert mtree != tree assert mtree.get_steiner_nodes() == [] assert set(mtree.get_arcs()) == \ set([('b', 'a'), ('a', 'c'), ('a', 'd')]) assert mpos != pos assert mpos == {}
def test_one_proper_steiner(spos, deg, edg): tree = SteinerTree('abcs', ['as', 'bs', 'cs'], { 'a': (0, 0), 'b': (2, 0), 'c': (0, 2) }) pos = {'s': spos} assert is_degenerate(tree, pos) == deg assert degenerate_edges(tree, pos) == edg mtree, mpos = merged(tree, pos) if deg: assert mtree != tree assert len(mtree.get_nodes()) == 3 assert len(mtree.get_arcs()) == 2 assert mpos == {} else: assert mtree == tree assert mpos == pos
def build_tree(nodes, raw_arcs, pos): _nodes = list(nodes) _arcs = [] _steiner_pos = {} num = 0 for ra in raw_arcs: if ra[1] == 'T': tail = nodes[int(ra[0])] else: # must be Steiner node coords = '_'.join(ra[0:2]) if coords in _steiner_pos: tail = _steiner_pos[coords] else: node = '_%d' % num _nodes.append(node) tail = _steiner_pos.setdefault(coords, node) num += 1 if ra[3] == 'T': head = nodes[int(ra[2])] else: # must be Steiner node coords = '_'.join(ra[2:4]) if coords in _steiner_pos: head = _steiner_pos[coords] else: node = '_%d' % num _nodes.append(node) head = _steiner_pos.setdefault(coords, node) num += 1 _arcs.append((tail, head)) tree = SteinerTree(_nodes, _arcs, pos) steiner_pos = {} for k, v in _steiner_pos.items(): node = v coords = k.split('_') steiner_pos[node] = float(coords[0]), float(coords[1]) return tree, steiner_pos
def test_length(): '''check Steiner tree geometric property for minimized length''' nodes = 'abcs' arcs = ['as', 'bs', 'cs'] terminals = {'a': (0, 0), 'b': (1, 0), 'c': (0, 1)} tree = SteinerTree(nodes, arcs, terminals) flow = find_arc_flow(tree, {'a': -4, 'b': 2, 'c': 2}) pos = steiner_pos(tree, flow, flow_exp=0.0) # check that fixed positions are kept for k, v in terminals.items(): assert_allclose(pos[k], v, atol=1e-7) # check (known) optimal position assert_allclose(pos['s'], (0.211, 0.211), atol=1e-3) # check angle property of Steiner node position # all angles are equal (to 120 deg = 2/3 pi) angles = star_angles('s', 'abc', pos) assert_allclose(angles, [2.0 / 3.0 * np.pi] * 3, atol=1e-4)
def test_make_forward_flow(): O = (0, 0) tree = SteinerTree('abcs', ['as', 'bs', 'cs'], {'a':O, 'b':O, 'c':O}) flow = {('a', 's'):20, ('b','s'):-5, ('c','s'):-15} new_tree, new_flow = make_forward_flow(tree, flow) assert set(tree.get_nodes()) == set(new_tree.get_nodes()) assert set(tree.get_terminal_nodes()) == set(new_tree.get_terminal_nodes()) assert len(tree.get_arcs()) == len(new_tree.get_arcs()) assert tree.get_terminal_positions() == new_tree.get_terminal_positions() assert ('a', 's') in new_tree.get_arcs() assert ('s', 'a') not in new_tree.get_arcs() assert ('s', 'b') in new_tree.get_arcs() assert ('b', 's') not in new_tree.get_arcs() assert ('s', 'c') in new_tree.get_arcs() assert ('c', 's') not in new_tree.get_arcs() assert all(new_flow[a] > 0.0 for a in new_tree.get_arcs())
def enum(term_pos, steiner_ids=None): '''Enumerate all representative full steiner trees. - term_pos: a dict mapping terminal node IDs to their (fixed) positions. - steiner_ids: optional list of Steiner node IDs. Must be of correct length. Following "Fampa et al.: A specialized branch-and-bound algorithm for the Euclidean Steiner tree problem in n-space", algorithm 3. ''' terms = sorted(term_pos.keys()) nt = len(terms) assert nt >= 3 if steiner_ids is None: steiner_ids = default_steiner_ids(nt) ns = nt - 2 assert len(steiner_ids) == ns # compute all representative trees connecting the Steiner nodes nclasses, reprtree = enum_Steiner_only(len(terms), steiner_ids) nc = nclasses[ns] # resulting data structure of representatives, indexed by label trees = {} # special case: 3 terminals, 1 Steiner node if nt == 3: s = steiner_ids[0] edges = [(t, s) for t in terms] tree = SteinerTree(terms + steiner_ids, edges, term_pos) trees[label_fst(tree)] = tree return trees # for each class of 'inner tree' for c in range(nc): tree = reprtree[ns, c] deg1 = [s for s in steiner_ids if tree.get_degree(s) == 1] deg2 = [s for s in steiner_ids if tree.get_degree(s) == 2] deg3 = [s for s in steiner_ids if tree.get_degree(s) == 3] assert 2 * len(deg1) + len(deg2) == nt cur_nodes = terms + steiner_ids cur_edges = tree.get_arcs() # (naive) enumeration of all permutations of terminals for perm in permutations(terms): # TODO: skip those permutations that yield the same tree, # i.e. where two terminals are swapped that are connected to # the same degree-1 Steiner node # connect all terminals to Steiner nodes in permutation order new_edges = [] K = len(deg1) for k in range(K): new_edges.append((perm[2 * k], deg1[k])) new_edges.append((perm[2 * k + 1], deg1[k])) for l in range(len(deg2)): new_edges.append((perm[2 * K + l], deg2[l])) new_tree = SteinerTree(cur_nodes, cur_edges + new_edges, term_pos) # check if isomorphic to saved tree label = label_fst(new_tree) if label in trees: # TODO: select lexicographically smaller tree continue # save tree trees[label] = new_tree return trees
def enum(term_pos, steiner_ids=None): '''Enumerate all representative full steiner trees. - term_pos: a dict mapping terminal node IDs to their (fixed) positions. - steiner_ids: optional list of Steiner node IDs. Must be of correct length. Following "Fampa et al.: A specialized branch-and-bound algorithm for the Euclidean Steiner tree problem in n-space", algorithm 3. ''' terms = sorted(term_pos.keys()) nt = len(terms) assert nt >= 3 if steiner_ids is None: steiner_ids = default_steiner_ids(nt) ns = nt - 2 assert len(steiner_ids) == ns # compute all representative trees connecting the Steiner nodes nclasses, reprtree = enum_Steiner_only(len(terms), steiner_ids) nc = nclasses[ns] # resulting data structure of representatives, indexed by label trees = {} # special case: 3 terminals, 1 Steiner node if nt == 3: s = steiner_ids[0] edges = [(t, s) for t in terms] tree = SteinerTree(terms + steiner_ids, edges, term_pos) trees[label_fst(tree)] = tree return trees # for each class of 'inner tree' for c in range(nc): tree = reprtree[ns, c] deg1 = [s for s in steiner_ids if tree.get_degree(s) == 1] deg2 = [s for s in steiner_ids if tree.get_degree(s) == 2] deg3 = [s for s in steiner_ids if tree.get_degree(s) == 3] assert 2*len(deg1) + len(deg2) == nt cur_nodes = terms + steiner_ids cur_edges = tree.get_arcs() # (naive) enumeration of all permutations of terminals for perm in permutations(terms): # TODO: skip those permutations that yield the same tree, # i.e. where two terminals are swapped that are connected to # the same degree-1 Steiner node # connect all terminals to Steiner nodes in permutation order new_edges = [] K = len(deg1) for k in range(K): new_edges.append((perm[2*k], deg1[k])) new_edges.append((perm[2*k + 1], deg1[k])) for l in range(len(deg2)): new_edges.append((perm[2*K + l], deg2[l])) new_tree = SteinerTree(cur_nodes, cur_edges + new_edges, term_pos) # check if isomorphic to saved tree label = label_fst(new_tree) if label in trees: # TODO: select lexicographically smaller tree continue # save tree trees[label] = new_tree return trees
def test_equality(): # testing just Net assert Net('', []) == Net('', []) assert not Net('', []) != Net('', []) assert Net('a', []) == Net('a', []) assert Net('a', []) != Net('', []) assert Net('a', []) != Net('A', []) assert Net('abc', ['ab']) == Net('abc', ['ab']) assert Net('abc', ['ab']) == Net('cba', ['ab']) assert Net('abc', ['ab']) != Net('abc', ['ba']) assert Net('abc', ['ab']) != Net('abc', ['bc']) assert Net('abc', ['ab', 'bc']) == Net('abc', ['bc', 'ab']) # testing SteinerTree with no terminals assert SteinerTree('', [], {}) == SteinerTree('', [], {}) assert not SteinerTree('', [], {}) != SteinerTree('', [], {}) assert SteinerTree('a', [], {}) == SteinerTree('a', [], {}) assert SteinerTree('a', [], {}) != SteinerTree('', [], {}) assert SteinerTree('a', [], {}) != SteinerTree('A', [], {}) assert SteinerTree('abc', ['ab'], {}) == SteinerTree('abc', ['ab'], {}) assert SteinerTree('abc', ['ab'], {}) == SteinerTree('cba', ['ab'], {}) assert SteinerTree('abc', ['ab'], {}) != SteinerTree('abc', ['ba'], {}) assert SteinerTree('abc', ['ab'], {}) != SteinerTree('abc', ['bc'], {}) assert SteinerTree('abc', ['ab', 'bc'], {}) == SteinerTree('abc', ['bc', 'ab'], {}) # testing SteinerTree with terminals assert SteinerTree('abc', ['ab'], {'a': 0}) == SteinerTree('abc', ['ab'], {'a': 0}) assert not SteinerTree('abc', ['ab'], {'a': 0}) != SteinerTree( 'abc', ['ab'], {'a': 0}) assert SteinerTree('abc', ['ab'], {'a': 0}) != SteinerTree( 'abc', ['ab'], {}) assert SteinerTree('abc', ['ab'], {'a': 0}) != SteinerTree( 'abc', ['ab'], {'a': 1})
def test_full_steiner_tree(): O = (0, 0) point = SteinerTree(['a'], [], {'a': O}) assert point.is_full_steiner_topology() line = SteinerTree('ab', ['ab'], {'a': O, 'b': O}) assert line.is_full_steiner_topology() path = SteinerTree('abs', ['as', 'sb'], {'a': O, 'b': O}) assert not path.is_full_steiner_topology() star = SteinerTree('abcs', ['as', 'bs', 'cs'], {'a': 0, 'b': O, 'c': O}) assert star.is_full_steiner_topology() V = SteinerTree('abc', ['ab', 'bc'], {'a': 0, 'b': O, 'c': O}) assert not V.is_full_steiner_topology() H = SteinerTree('abcdst', ['as', 'bs', 'st', 'ct', 'dt'], { 'a': O, 'b': O, 'c': O, 'd': O }) assert H.is_full_steiner_topology() X = SteinerTree('abcds', ['as', 'bs', 'cs', 'ds'], { 'a': O, 'b': O, 'c': O, 'd': O }) assert not X.is_full_steiner_topology()