gcsopt 0.1.0__tar.gz
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.
- gcsopt-0.1.0/LICENSE +21 -0
- gcsopt-0.1.0/PKG-INFO +17 -0
- gcsopt-0.1.0/README.md +7 -0
- gcsopt-0.1.0/examples/__init__.py +0 -0
- gcsopt-0.1.0/examples/inspection.py +186 -0
- gcsopt-0.1.0/gcsopt/__init__.py +4 -0
- gcsopt-0.1.0/gcsopt/edges.py +98 -0
- gcsopt-0.1.0/gcsopt/graph_problems/__init__.py +0 -0
- gcsopt-0.1.0/gcsopt/graph_problems/facility_location.py +44 -0
- gcsopt-0.1.0/gcsopt/graph_problems/from_ilp.py +136 -0
- gcsopt-0.1.0/gcsopt/graph_problems/minimum_spanning_tree.py +109 -0
- gcsopt-0.1.0/gcsopt/graph_problems/minimum_spanning_tree_gurobipy.py +135 -0
- gcsopt-0.1.0/gcsopt/graph_problems/rounding/__init__.py +0 -0
- gcsopt-0.1.0/gcsopt/graph_problems/rounding/shortest_path.py +82 -0
- gcsopt-0.1.0/gcsopt/graph_problems/shortest_path.py +45 -0
- gcsopt-0.1.0/gcsopt/graph_problems/traveling_salesman.py +63 -0
- gcsopt-0.1.0/gcsopt/graph_problems/traveling_salesman_gurobipy.py +68 -0
- gcsopt-0.1.0/gcsopt/graph_problems/utils.py +68 -0
- gcsopt-0.1.0/gcsopt/graph_problems/utils_gurobipy.py +184 -0
- gcsopt-0.1.0/gcsopt/graphs.py +439 -0
- gcsopt-0.1.0/gcsopt/plot_utils.py +103 -0
- gcsopt-0.1.0/gcsopt/programs.py +231 -0
- gcsopt-0.1.0/gcsopt/vertices.py +24 -0
- gcsopt-0.1.0/gcsopt.egg-info/PKG-INFO +17 -0
- gcsopt-0.1.0/gcsopt.egg-info/SOURCES.txt +32 -0
- gcsopt-0.1.0/gcsopt.egg-info/dependency_links.txt +1 -0
- gcsopt-0.1.0/gcsopt.egg-info/requires.txt +3 -0
- gcsopt-0.1.0/gcsopt.egg-info/top_level.txt +3 -0
- gcsopt-0.1.0/setup.cfg +4 -0
- gcsopt-0.1.0/setup.py +16 -0
- gcsopt-0.1.0/tests/__init__.py +0 -0
- gcsopt-0.1.0/tests/test_graph_problems.py +295 -0
- gcsopt-0.1.0/tests/test_graphs.py +84 -0
- gcsopt-0.1.0/tests/test_programs.py +411 -0
gcsopt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Tobia Marcucci
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gcsopt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gcsopt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Library for solving optimization problems over Graphs of Convex Sets (GCS).
|
|
5
|
+
Author: Tobia Marcucci
|
|
6
|
+
Author-email: marcucci@ucsb.edu
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: cvxpy>=1.5
|
|
11
|
+
Requires-Dist: pytest
|
|
12
|
+
Dynamic: author
|
|
13
|
+
Dynamic: author-email
|
|
14
|
+
Dynamic: license
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
Dynamic: requires-dist
|
|
17
|
+
Dynamic: summary
|
gcsopt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# GCSOPT
|
|
2
|
+
|
|
3
|
+
Library based on [CVXPY](https://www.cvxpy.org) to solve optimization problems in Graphs of Convex Sets (GCS).
|
|
4
|
+
For a detailed description of the algorithms implemented implemented in this library see the PhD thesis [Graphs of Convex Sets with Applications to Optimal Control and Motion Planning
|
|
5
|
+
](https://groups.csail.mit.edu/robotics-center/public_papers/Marcucci24a.pdf).
|
|
6
|
+
|
|
7
|
+
**Warning:** The library is still under development and not thoroughly tested yet.
|
|
File without changes
|
|
@@ -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()
|
|
@@ -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)
|