gcsopt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
examples/__init__.py ADDED
File without changes
examples/inspection.py ADDED
@@ -0,0 +1,186 @@
1
+ import cvxpy as cp
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import matplotlib.patches as patches
5
+ from gcsopt import GraphOfConvexSets
6
+
7
+ # all the rooms in the floor plant
8
+ # each room is described by a triplet (index, lower corner, upper corner)
9
+ rooms = [
10
+ (0, [0, 5], [2, 8]),
11
+ (1, [0, 3], [4, 5]),
12
+ (2, [0, 0], [2, 3]),
13
+ (3, [2, 5], [4, 8]),
14
+ (4, [2, 0], [4, 3]),
15
+ (5, [4, 0], [6, 8]),
16
+ (6, [6, 7], [8, 8]),
17
+ (7, [6, 1], [8, 7]),
18
+ (8, [6, 0], [8, 1]),
19
+ (9, [8, 0], [10, 10]),
20
+ (10, [10, 5], [12, 10]),
21
+ (11, [10, 4], [12, 5]),
22
+ (12, [10, 0], [12, 4]),
23
+ (13, [12, 0], [16, 2]),
24
+ (14, [12, 2], [14, 10]),
25
+ (15, [14, 7], [16, 10]),
26
+ (17, [0, 8], [4, 10]),
27
+ (18, [4, 8], [8, 10]),
28
+ (19, [14, 2], [16, 7]),
29
+ ]
30
+
31
+ # all the doors in the floor plant
32
+ # each door is described by
33
+ # (first room index, second room index, door lower corner, door upper corner)
34
+ doors = [
35
+ (0, 3, [2, 7], [2, 8]),
36
+ (1, 2, [1, 3], [2, 3]),
37
+ (1, 5, [4, 4], [4, 5]),
38
+ (2, 4, [2, 2], [2, 3]),
39
+ (3, 5, [4, 5], [4, 8]),
40
+ (3, 17, [3, 8], [4, 8]),
41
+ (4, 5, [4, 0], [4, 3]),
42
+ (5, 6, [6, 7], [6, 8]),
43
+ (5, 7, [6, 1], [6, 2]),
44
+ (5, 8, [6, 0], [6, 1]),
45
+ (6, 9, [8, 7], [8, 8]),
46
+ (7, 9, [8, 3], [8, 4]),
47
+ (8, 9, [8, 0], [8, 1]),
48
+ (9, 10, [10, 7], [10, 8]),
49
+ (9, 11, [10, 4], [10, 5]),
50
+ (9, 12, [10, 3], [10, 4]),
51
+ (9, 18, [8, 9], [8, 10]),
52
+ (10, 14, [12, 5], [12, 6]),
53
+ (11, 14, [12, 4], [12, 5]),
54
+ (12, 13, [12, 0], [12, 2]),
55
+ (14, 15, [14, 7], [14, 8]),
56
+ (14, 19, [14, 6], [14, 7]),
57
+ ]
58
+
59
+ # rooms that must be visited by the minimum-length trajectory
60
+ visit_rooms = [0, 2, 7, 10, 13, 15, 18]
61
+
62
+ # helper class that allows to construct a floor
63
+ class Floor(GraphOfConvexSets):
64
+
65
+ def __init__(self, rooms, doors, name):
66
+ super().__init__()
67
+ self.rooms = rooms
68
+ self.doors = doors
69
+ self.name = name
70
+ for room in rooms:
71
+ self.add_room(*room)
72
+ for door in doors:
73
+ self.add_door(*door)
74
+
75
+ def add_room(self, n, l, u):
76
+ v = self.add_vertex(f"{self.name}_{n}")
77
+ x1 = v.add_variable(2)
78
+ x2 = v.add_variable(2)
79
+ v.add_constraints([x1 >= l, x1 <= u])
80
+ v.add_constraints([x2 >= l, x2 <= u])
81
+ v.add_cost(cp.norm2(x2 - x1))
82
+ return v
83
+
84
+ def add_one_way_door(self, n, m, l, u):
85
+ tail = self.get_vertex(f"{self.name}_{n}")
86
+ head = self.get_vertex(f"{self.name}_{m}")
87
+ e = self.add_edge(tail, head)
88
+ e.add_constraint(tail.variables[1] == head.variables[0])
89
+ e.add_constraint(tail.variables[1] >= l)
90
+ e.add_constraint(tail.variables[1] <= u)
91
+ return e
92
+
93
+ def add_door(self, n, m, l, u):
94
+ e1 = self.add_one_way_door(n, m, l, u)
95
+ e2 = self.add_one_way_door(m, n, l, u)
96
+ return e1, e2
97
+
98
+ # initialize empty graph
99
+ graph = GraphOfConvexSets()
100
+
101
+ # adds one copy of the floor plant for each room that we must visit
102
+ num_floors = len(visit_rooms)
103
+ for floor in range(num_floors):
104
+ graph.add_disjoint_subgraph(Floor(rooms, doors, floor))
105
+
106
+ # connects copies of the floors on a given room
107
+ def connect_floors(floor1, floor2, room):
108
+ tail = graph.get_vertex(f"{floor1}_{room}")
109
+ head = graph.get_vertex(f"{floor2}_{room}")
110
+ edge = graph.add_edge(tail, head)
111
+ edge.add_constraint(tail.variables[1] == head.variables[0])
112
+
113
+ # connect top floor to ground floor at first visit room
114
+ first_room = visit_rooms[0]
115
+ first_floor = 0
116
+ last_floor = num_floors - 1
117
+ connect_floors(last_floor, first_floor, first_room)
118
+
119
+ # connect each floor to the floor above at the visit room
120
+ for floor in range(last_floor):
121
+ for room in visit_rooms[1:]:
122
+ connect_floors(floor, floor + 1, room)
123
+
124
+ # retrieve binary variables
125
+ yv = graph.vertex_binaries()
126
+ ye = graph.edge_binaries()
127
+
128
+ # constraints of the integer programming formulation
129
+ ilp_constraints = []
130
+ for i, vertex in enumerate(graph.vertices):
131
+ inc_edges = graph.incoming_edge_indices(vertex)
132
+ out_edges = graph.outgoing_edge_indices(vertex)
133
+ ilp_constraints.append(yv[i] == sum(ye[inc_edges]))
134
+ ilp_constraints.append(yv[i] == sum(ye[out_edges]))
135
+
136
+ # returns the edge binary variable that connects two floors through a given room
137
+ def get_binary_variable(floor1, floor2, room):
138
+ tail_name = f"{floor1}_{room}"
139
+ head_name = f"{floor2}_{room}"
140
+ edge = graph.get_edge(tail_name, head_name)
141
+ return ye[graph.edge_index(edge)]
142
+
143
+ # add constraints that force the trajectory to move between floors
144
+ ilp_constraints.append(get_binary_variable(last_floor, first_floor, first_room) == 1)
145
+ for room in visit_rooms[1:]:
146
+ flow = sum(get_binary_variable(floor, floor + 1, room) for floor in range(last_floor))
147
+ ilp_constraints.append(flow == 1)
148
+
149
+ # solve problem (this will take a very long time)
150
+ prob = graph.solve_from_ilp(ilp_constraints)
151
+ print('Problem status:', prob.status)
152
+ print('Optimal value:', prob.value)
153
+
154
+ # plot solution
155
+ plt.figure()
156
+ plt.axis("equal")
157
+
158
+ # helper function that plots one room
159
+ def plot_room(n, l, u):
160
+ l = np.array(l)
161
+ u = np.array(u)
162
+ d = u - l
163
+ fc = "mistyrose" if n in visit_rooms else "mintcream"
164
+ rect = patches.Rectangle(l, *d, fc=fc, ec="k")
165
+ plt.gca().add_patch(rect)
166
+
167
+ # helper function that plots one door
168
+ def plot_door(l, u):
169
+ endpoints = np.array([l, u]).T
170
+ plt.plot(*endpoints, color="mintcream", solid_capstyle="butt")
171
+ plt.plot(*endpoints, color="grey", linestyle=":")
172
+
173
+ # plot all rooms and doors
174
+ for room in rooms:
175
+ plot_room(*room)
176
+ for door in doors:
177
+ plot_door(door[2], door[3])
178
+
179
+ # plot optimal trajectory
180
+ for vertex in graph.vertices:
181
+ if np.isclose(vertex.binary_variable.value, 1):
182
+ x1, x2 = vertex.variables
183
+ values = np.array([x1.value, x2.value]).T
184
+ plt.plot(*values, c="b", linestyle="--")
185
+
186
+ plt.show()
gcsopt/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ __version__ = "0.1.0"
2
+ __author__ = "Tobia Marcucci"
3
+
4
+ from gcsopt.graphs import GraphOfConicSets, GraphOfConvexSets
gcsopt/edges.py ADDED
@@ -0,0 +1,98 @@
1
+ import numpy as np
2
+ import cvxpy as cp
3
+ from gcsopt.programs import ConicProgram, ConvexProgram
4
+
5
+ class ConicEdge(ConicProgram):
6
+
7
+ def __init__(self, tail, head, size, id_to_range=None, binary_variable=None):
8
+
9
+ # Check inputs.
10
+ super().__init__(size, id_to_range, binary_variable)
11
+ self.slack_size = size - tail.size - head.size
12
+ if self.slack_size < 0:
13
+ raise ValueError(
14
+ f"Size mismatch: edge.size = {size}, tail.size = {tail.size}, "
15
+ f"head.size = {head.size}. Size of the edge must be larger "
16
+ "than the sum of tail and head sizes.")
17
+
18
+ # Store inputs.
19
+ self.tail = tail
20
+ self.head = head
21
+ self.name = (tail.name, head.name)
22
+
23
+ def _check_vector_sizes(self, xv, xw, xe):
24
+ sizes = (xv.size, xw.size, xe.size)
25
+ expected_sizes = (self.tail.size, self.head.size, self.slack_size)
26
+ if sizes != expected_sizes:
27
+ ValueError(
28
+ f"Size mismatch. Got vectors of size {sizes}. Expected vectors "
29
+ f"of size {expected_sizes}.")
30
+
31
+ def _concatenate(self, xv, xw, xe):
32
+ if self.slack_size == 0:
33
+ return cp.hstack((xv, xw))
34
+ else:
35
+ return cp.hstack((xv, xw, xe))
36
+
37
+ def cost_homogenization(self, xv, xw, xe, y):
38
+ self._check_vector_sizes(xv, xw, xe)
39
+ x = self._concatenate(xv, xw, xe)
40
+ return super().cost_homogenization(x, y)
41
+
42
+ def constraint_homogenization(self, xv, xw, xe, y):
43
+ self._check_vector_sizes(xv, xw, xe)
44
+ x = self._concatenate(xv, xw, xe)
45
+ return super().constraint_homogenization(x, y)
46
+
47
+ class ConvexEdge(ConvexProgram):
48
+
49
+ def __init__(self, tail, head):
50
+ super().__init__()
51
+ self.tail = tail
52
+ self.head = head
53
+ self.name = (self.tail.name, self.head.name)
54
+
55
+ def to_conic(self, conic_tail, conic_head):
56
+
57
+ # Include tail and head variables in id_to_range. Variable order is
58
+ # x_tail, x_head, a dn then x_edge. Start with copy of tail dictionary.
59
+ id_to_range = conic_tail.id_to_range.copy()
60
+
61
+ # Add copy of the head dictionary shifted by the size of the tail.
62
+ offset = conic_tail.size
63
+ for id, r in conic_head.id_to_range.items():
64
+ id_to_range[id] = range(r.start + offset, r.stop + offset)
65
+
66
+ # Add to dictionary variables that are associated with this edge if they
67
+ # are not in the tail or head dictionary yet.
68
+ conic_program = super().to_conic()
69
+ offset = conic_tail.size + conic_head.size
70
+ for id, r in conic_program.id_to_range.items():
71
+ if not id in id_to_range:
72
+ id_to_range[id] = range(r.start + offset, r.stop + offset)
73
+
74
+ # Initialize empty edge program.
75
+ size = max([r.stop for r in id_to_range.values()])
76
+ conic_edge = ConicEdge(
77
+ conic_tail,
78
+ conic_head,
79
+ size,
80
+ id_to_range,
81
+ conic_program.binary_variable)
82
+
83
+ # Reorder matrices and extend them with zeros according to the new
84
+ # id_to_range dictionary.
85
+ c = np.zeros(size)
86
+ A = np.zeros((conic_program.A.shape[0], size))
87
+ for id, r in conic_program.id_to_range.items():
88
+ c[id_to_range[id]] = conic_program.c[r]
89
+ A[:, id_to_range[id]] = conic_program.A[:, r]
90
+
91
+ # Assemble conic edge.
92
+ conic_edge.add_cost(c, conic_program.d)
93
+ conic_edge.add_constraints(A, conic_program.b, conic_program.K)
94
+ return conic_edge
95
+
96
+ def _check_variables_are_defined(self, variables):
97
+ defined_variables = self.variables + self.tail.variables + self.head.variables
98
+ super()._check_variables_are_defined(variables, defined_variables)
File without changes
@@ -0,0 +1,44 @@
1
+ import cvxpy as cp
2
+ import numpy as np
3
+ from gcsopt.graph_problems.utils import define_variables, enforce_edge_programs, get_solution
4
+
5
+ def facility_location(conic_graph, binary, tol, **kwargs):
6
+
7
+ # define variables
8
+ yv, zv, ye, ze, ze_tail, ze_head = define_variables(conic_graph, binary)
9
+
10
+ # edge costs and constraints
11
+ cost, constraints = enforce_edge_programs(conic_graph, ye, ze, ze_tail, ze_head)
12
+
13
+ # constraints on the vertices
14
+ for i, vertex in enumerate(conic_graph.vertices):
15
+ inc = conic_graph.incoming_edge_indices(vertex)
16
+ out = conic_graph.outgoing_edge_indices(vertex)
17
+
18
+ # check that graph topology is correct
19
+ if len(inc) > 0 and len(out) > 0:
20
+ raise ValueError("Graph is not bipartite.")
21
+
22
+ # user vertex
23
+ if len(inc) > 0:
24
+ cost += vertex.cost_homogenization(zv[i], 1)
25
+ constraints += [
26
+ yv[i] == 1,
27
+ sum(ye[inc]) == 1,
28
+ sum(ze_head[inc]) == zv[i]]
29
+
30
+ # facility vertex
31
+ else:
32
+ cost += vertex.cost_homogenization(zv[i], yv[i])
33
+ constraints.append(yv[i] <= 1)
34
+
35
+ # constraints on the edges
36
+ for k, edge in enumerate(conic_graph.edges):
37
+ i = conic_graph.vertex_index(edge.tail)
38
+ constraints += edge.tail.constraint_homogenization(zv[i] - ze_tail[k], yv[i] - ye[k])
39
+
40
+ # solve problem
41
+ prob = cp.Problem(cp.Minimize(cost), constraints)
42
+ prob.solve(**kwargs)
43
+
44
+ return get_solution(conic_graph, prob, ye, ze, yv, zv, tol)
@@ -0,0 +1,136 @@
1
+ import cvxpy as cp
2
+ import numpy as np
3
+ from gcsopt.programs import ConvexProgram
4
+ from gcsopt.graph_problems.utils import define_variables, get_solution
5
+
6
+ def from_ilp(conic_graph, ilp_constraints, binary, tol, **kwargs):
7
+
8
+ # Put given constraints in conic form. Next lines are not nice but I cannot
9
+ # use convex_ilp.add_variables.
10
+ convex_ilp = ConvexProgram()
11
+ vertex_binaries = conic_graph.vertex_binaries()
12
+ edge_binaries = conic_graph.edge_binaries()
13
+ convex_ilp.variables.extend(vertex_binaries)
14
+ convex_ilp.variables.extend(edge_binaries)
15
+ convex_ilp.add_constraints(ilp_constraints)
16
+ conic_ilp = convex_ilp.to_conic()
17
+
18
+ # Indices of vertex and edge binaries in conic program. Uses the fact that
19
+ # binaries are scalars.
20
+ vertex_indices = [conic_ilp.id_to_range[y.id].start for y in vertex_binaries]
21
+ edge_indices = [conic_ilp.id_to_range[y.id].start for y in edge_binaries]
22
+ Av = conic_ilp.A[:, vertex_indices]
23
+ Ae = conic_ilp.A[:, edge_indices]
24
+
25
+ # Variables of MICP. Note that xv and xe can always be omitted.
26
+ yv, zv, ye, ze, ze_tail, ze_head = define_variables(conic_graph, binary)
27
+
28
+ # Vertex costs and constraints.
29
+ cost = 0
30
+ constraints = [yv <= 1]
31
+ for i, vertex in enumerate(conic_graph.vertices):
32
+ cost += vertex.cost_homogenization(zv[i], yv[i])
33
+
34
+ # Edge costs and constraints.
35
+ for k, edge in enumerate(conic_graph.edges):
36
+ cost += edge.cost_homogenization(ze_tail[k], ze_head[k], ze[k], ye[k])
37
+ constraints += edge.constraint_homogenization(ze_tail[k], ze_head[k], ze[k], ye[k])
38
+
39
+ # Enforce constraint implied by the subgraph polytope: 0 <= ye <= yv.
40
+ # Letting the user decide when to enforce these is error prone.
41
+ constraints += edge.tail.constraint_homogenization(ze_tail[k], ye[k])
42
+ constraints += edge.head.constraint_homogenization(ze_head[k], ye[k])
43
+ i = conic_graph.vertex_index(edge.tail)
44
+ j = conic_graph.vertex_index(edge.head)
45
+ constraints += edge.tail.constraint_homogenization(zv[i] - ze_tail[k], yv[i] - ye[k])
46
+ constraints += edge.head.constraint_homogenization(zv[j] - ze_head[k], yv[j] - ye[k])
47
+
48
+ # Check each line of each conic constraint.
49
+ start = 0
50
+ for K, size in conic_ilp.K:
51
+ stop = start + size
52
+ for j in range(start, stop):
53
+
54
+ # Evaluate affine constraint using the problem binaries.
55
+ av = Av[j]
56
+ ae = Ae[j]
57
+ bj = conic_ilp.b[j]
58
+
59
+ # If there are no shared vertices, just enforce scalar constraint.
60
+ shared_vertices = find_shared_vertices(conic_graph, av, ae)
61
+ if not shared_vertices:
62
+ constraints.append(K(av @ yv + ae @ ye + bj))
63
+
64
+ # If there are shared vertices, apply constraint-generation lemma to
65
+ # each affine constraint.
66
+ for vertex in shared_vertices:
67
+
68
+ # Assemble implied constraint.
69
+ i = conic_graph.vertex_index(vertex)
70
+ inc = conic_graph.incoming_edge_indices(vertex)
71
+ out = conic_graph.outgoing_edge_indices(vertex)
72
+ lhs = (bj + av[i]) * yv[i] + ae @ ye
73
+ vector_lhs = (bj + av[i]) * zv[i]
74
+ vector_lhs += sum(ae[inc] * ze_head[inc])
75
+ vector_lhs += sum(ae[out] * ze_tail[out])
76
+
77
+ # Enforce implied constraints.
78
+ if K == cp.Zero:
79
+ constraints += [lhs == 0, vector_lhs == 0]
80
+ if not np.isclose(bj, 0):
81
+ constraints.append(yv[i] == 1)
82
+ elif K == cp.NonNeg:
83
+ constraints += vertex.constraint_homogenization(vector_lhs, lhs)
84
+ if bj < 0:
85
+ constraints.append(yv[i] == 1)
86
+ elif K == cp.NonPos:
87
+ constraints += vertex.constraint_homogenization(-vector_lhs, -lhs)
88
+ if bj > 0:
89
+ constraints.append(yv[i] == 1)
90
+ else:
91
+ raise ValueError(
92
+ "All the constraints of ILP must be affine. Got cone "
93
+ f"of type {type(K)}.")
94
+
95
+ # Shift row indices.
96
+ start = stop
97
+
98
+ # Solve problem.
99
+ prob = cp.Problem(cp.Minimize(cost), constraints)
100
+ prob.solve(**kwargs)
101
+
102
+ return get_solution(conic_graph, prob, ye, ze, yv, zv, tol)
103
+
104
+ def find_shared_vertices(conic_graph, av, ae):
105
+ """
106
+ Checks if the linear function
107
+ av^T y_v + ae^T y_e
108
+ is amenable to the lemma that is used to generate spatial constraints
109
+ (see Lemma 5.1 from thesis). To this end, it should be possible to rewrite
110
+ the linear function above as
111
+ b y_v_i + sum_{k in edges_incident_i} c_k y_e_k
112
+ where i is the index of a vertex and k is the index of an edge incident with
113
+ the ith vertex. This function returns all the values of i that allow such
114
+ a decomposition.
115
+ """
116
+
117
+ # Extract nonzero vertices and edges.
118
+ nonzero_vertices = [conic_graph.vertices[i] for i in np.nonzero(av)[0]]
119
+ nonzero_edges = [conic_graph.edges[k] for k in np.nonzero(ae)[0]]
120
+
121
+ # Compute shared vertices among all edges.
122
+ tails_and_heads = [{edge.tail, edge.head} for edge in nonzero_edges]
123
+ shared_vertices = set.intersection(*tails_and_heads) if tails_and_heads else set()
124
+
125
+ # If av is zero, return all the vertices shared by the edges.
126
+ if not nonzero_vertices:
127
+ return list(shared_vertices)
128
+
129
+ # If av has one nonzero entry, there is only one candidate vertex, and
130
+ # no other shared vertex can be different from it.
131
+ elif len(nonzero_vertices) == 1 and shared_vertices <= set(nonzero_vertices):
132
+ return nonzero_vertices
133
+
134
+ # In all other cases, the decomposition is not possible.
135
+ else:
136
+ return []
@@ -0,0 +1,109 @@
1
+ import cvxpy as cp
2
+ import numpy as np
3
+ from itertools import combinations
4
+ from gcsopt.graph_problems.utils import (define_variables, enforce_edge_programs,
5
+ get_solution, subtour_elimination_constraints)
6
+
7
+ def undirected_minimum_spanning_tree(conic_graph, subtour_elimination, binary, tol, **kwargs):
8
+ """
9
+ Here we use the subtour-elimination formulation, which is also perfect.
10
+ """
11
+
12
+ # Check that graph is undirected.
13
+ if conic_graph.directed:
14
+ raise ValueError("Called MSTP for undirected graphs on a directed graph.")
15
+
16
+ # Define variables.
17
+ yv, zv, ye, ze, ze_tail, ze_head = define_variables(conic_graph, binary)
18
+
19
+ # Edge costs and constraints.
20
+ cost, constraints = enforce_edge_programs(conic_graph, ye, ze, ze_tail, ze_head)
21
+
22
+ # Number of edges in the tree.
23
+ constraints.append(sum(ye) == conic_graph.num_vertices() - 1)
24
+
25
+ # Vertex costs.
26
+ for i, vertex in enumerate(conic_graph.vertices):
27
+ cost += vertex.cost_homogenization(zv[i], 1)
28
+
29
+ # Cutset constraints for one vertex only.
30
+ incident = conic_graph.incident_edge_indices(vertex)
31
+ inc = [k for k in incident if conic_graph.edges[k].head == vertex]
32
+ out = [k for k in incident if conic_graph.edges[k].tail == vertex]
33
+ constraints += vertex.constraint_homogenization(
34
+ sum(ze_head[inc]) + sum(ze_tail[out]) - zv[i],
35
+ sum(ye[incident]) - 1)
36
+
37
+ # Constraints implied by ye <= 1.
38
+ for k in inc:
39
+ constraints += vertex.constraint_homogenization(zv[i] - ze_head[k], 1 - ye[k])
40
+ for k in out:
41
+ constraints += vertex.constraint_homogenization(zv[i] - ze_tail[k], 1 - ye[k])
42
+
43
+ # Exponentially many subtour elimination constraints.
44
+ if subtour_elimination:
45
+ constraints += subtour_elimination_constraints(conic_graph, ye)
46
+
47
+ # Solve problem.
48
+ prob = cp.Problem(cp.Minimize(cost), constraints)
49
+ prob.solve(**kwargs)
50
+
51
+ # Set value of vertex binaries.
52
+ if prob.status == "optimal":
53
+ yv.value = np.ones(conic_graph.num_vertices())
54
+
55
+ return get_solution(conic_graph, prob, ye, ze, yv, zv, tol)
56
+
57
+ def directed_minimum_spanning_tree(conic_graph, conic_root, subtour_elimination, binary, tol, **kwargs):
58
+ """
59
+ This is the cutset formulation of the directed MSTP. Unlike the undirected
60
+ case, this formulation is perfect for a directed graph, see
61
+ https://www.cs.cmu.edu/afs/cs.cmu.edu/academic/class/15850-f20/www/notes/lec2.pdf.
62
+ """
63
+
64
+ # Check that graph is directed.
65
+ if not conic_graph.directed:
66
+ raise ValueError("Called MSTP for directed graphs on an undirected graph.")
67
+
68
+ # Define variables.
69
+ yv, zv, ye, ze, ze_tail, ze_head = define_variables(conic_graph, binary)
70
+
71
+ # Edge basic costs and constraints.
72
+ cost, constraints = enforce_edge_programs(conic_graph, ye, ze, ze_tail, ze_head)
73
+
74
+ # Cost and constraints on the vertices.
75
+ for i, vertex in enumerate(conic_graph.vertices):
76
+ cost += vertex.cost_homogenization(zv[i], 1)
77
+
78
+ # Constraints on incoming edges.
79
+ inc = conic_graph.incoming_edge_indices(vertex)
80
+ if vertex == conic_root:
81
+ constraints += vertex.constraint_homogenization(zv[i], 1)
82
+ constraints += [ye[k] == 0 for k in inc]
83
+ constraints += [ze_head[k] == 0 for k in inc]
84
+ else:
85
+ constraints += [sum(ye[inc]) == 1, sum(ze_head[inc]) == zv[i]]
86
+
87
+ # Constraints on outgoing edges.
88
+ for k in conic_graph.outgoing_edge_indices(vertex):
89
+ constraints += vertex.constraint_homogenization(zv[i] - ze_tail[k], 1 - ye[k])
90
+
91
+ # Cutset constraints for all subsets of vertices with cardinality between
92
+ # 2 and num_vertices - 1.
93
+ if subtour_elimination:
94
+ i = conic_graph.vertex_index(conic_root)
95
+ subvertices = conic_graph.vertices[:i] + conic_graph.vertices[i+1:]
96
+ for subtour_size in range(2, conic_graph.num_vertices()):
97
+ for vertices in combinations(subvertices, subtour_size):
98
+ inc = conic_graph.incoming_edge_indices(vertices)
99
+ constraints.append(sum(ye[inc]) >= 1)
100
+
101
+ # Solve problem.
102
+ prob = cp.Problem(cp.Minimize(cost), constraints)
103
+ prob.solve(**kwargs)
104
+
105
+ # Set value of vertex binaries.
106
+ if prob.status == "optimal":
107
+ yv.value = np.ones(conic_graph.num_vertices())
108
+
109
+ return get_solution(conic_graph, prob, ye, ze, yv, zv, tol)