Skip to content

Commit 8f4a608

Browse files
Added min set cover ILP model
1 parent 91db3b1 commit 8f4a608

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

examples/min_set_cover.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import flowpaths as fp
2+
3+
def main():
4+
universe = [1,2,3,4,5]
5+
subsets = [{1,2,3}, {2,4}, {3,5}, {1,4,5}]
6+
subset_weights = [1,2,1,1]
7+
8+
msc_solver = fp.MinSetCover(
9+
universe=universe,
10+
subsets=subsets,
11+
subset_weights=subset_weights
12+
)
13+
14+
msc_solver.solve()
15+
process_solution(msc_solver)
16+
17+
def process_solution(model: fp.MinGenSet):
18+
if model.is_solved():
19+
print(model.get_solution(as_subsets=True))
20+
else:
21+
print("Model could not be solved.")
22+
print(model.solve_statistics)
23+
24+
if __name__ == "__main__":
25+
main()

flowpaths/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .nodeexpandeddigraph import NodeExpandedDiGraph
99
from .utils import graphutils as graphutils
1010
from .mingenset import MinGenSet
11+
from .minsetcover import MinSetCover
1112

1213
__all__ = [
1314
"AbstractPathModelDAG",
@@ -20,4 +21,5 @@
2021
"NodeExpandedDiGraph",
2122
"graphutils",
2223
"MinGenSet",
24+
"MinSetCover",
2325
]

flowpaths/minsetcover.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import flowpaths.utils.solverwrapper as sw
2+
import time
3+
4+
class MinSetCover():
5+
def __init__(
6+
self,
7+
universe: list,
8+
subsets: list,
9+
subset_weights: list = None
10+
):
11+
"""
12+
This class solves the minimum set cover problem. Given a universe `universe` and a list of subsets `subsets`,
13+
the goal is to find the smallest list of subsets `set_cover` such that:
14+
15+
- every element in `universe` is in at least one subset in `set_cover`.
16+
- the sum of the weights of the subsets in `set_cover` is minimized.
17+
18+
Parameters
19+
----------
20+
21+
- `universe` : list`
22+
23+
The universe of elements that must be covered.
24+
25+
- `subsets : list`
26+
27+
A list of subsets that can be used to cover the universe.
28+
29+
- `subset_weights : list`
30+
31+
The weight of each subset, as a list in the same order that the subsets appear in the list `subsets`.
32+
If not provided, each subset is assumed to have a weight of 1.
33+
34+
"""
35+
36+
self.universe = universe
37+
self.subsets = subsets
38+
self.subset_weights = subset_weights
39+
self.set_cover = []
40+
self.set_cover_indices = []
41+
self.set_cover_weights = []
42+
43+
self.__is_solved = None
44+
self.__solution = None
45+
46+
self.__encode_set_cover()
47+
48+
def __encode_set_cover(self):
49+
"""
50+
This function encodes the set cover problem as an integer linear program.
51+
"""
52+
self.solver = sw.SolverWrapper()
53+
54+
self.subset_indexes = [(i) for i in range(len(self.subsets))]
55+
56+
self.subset_vars = self.solver.add_variables(
57+
self.subset_indexes,
58+
name_prefix="subset",
59+
lb=0,
60+
ub=1,
61+
var_type="integer"
62+
)
63+
64+
# Every element of the universe must be in at least one subset
65+
for element in self.universe:
66+
self.solver.add_constraint(
67+
self.solver.quicksum(
68+
self.subset_vars[i]
69+
for i in range(len(self.subsets)) if element in self.subsets[i]
70+
)
71+
>= 1,
72+
name=f"total",
73+
)
74+
75+
# Objective function
76+
self.solver.set_objective(
77+
self.solver.quicksum(
78+
self.subset_weights[i] * self.subset_vars[i]
79+
for i in range(len(self.subsets))
80+
)
81+
)
82+
83+
def solve(self) -> bool:
84+
"""
85+
Solves the minimum set cover problem.
86+
87+
Returns
88+
-------
89+
- bool
90+
91+
`True` if the model was solved, `False` otherwise.
92+
"""
93+
start_time = time.time()
94+
95+
self.solver.optimize()
96+
if self.solver.get_model_status() == "kOptimal":
97+
subset_cover_sol = self.solver.get_variable_values("subset", [int])
98+
self.__solution = [i for i in range(len(self.subsets)) if subset_cover_sol[i] == 1]
99+
self.__is_solved = True
100+
self.solve_statistics = {
101+
"solve_time": time.time() - start_time,
102+
"num_elements": len(self.__solution),
103+
"status": self.solver.get_model_status(),
104+
}
105+
return True
106+
else:
107+
self.solve_statistics = {
108+
"solve_time": time.time() - start_time,
109+
"status": self.solver.get_model_status(),
110+
}
111+
return False
112+
113+
def is_solved(self):
114+
"""
115+
Returns `True` if the model was solved, `False` otherwise.
116+
"""
117+
if self.__is_solved is None:
118+
raise Exception("Model not yet solved. If you want to solve it, call the `solve` method first.")
119+
120+
return self.__is_solved
121+
122+
def check_is_solved(self):
123+
if not self.is_solved():
124+
raise Exception(
125+
"Model not solved. If you want to solve it, call the solve method first. \
126+
If you already ran the solve method, then the model is infeasible, or you need to increase parameter time_limit."
127+
)
128+
129+
def get_solution(self, as_subsets: bool = False):
130+
"""
131+
Returns the solution to the minimum generating set problem, if the model was solved.
132+
133+
!!! warning "Warning"
134+
Call the `solve` method first.
135+
"""
136+
if self.__solution is not None:
137+
if not as_subsets:
138+
return self.__solution
139+
else:
140+
return [self.subsets[i] for i in self.__solution]
141+
142+
self.check_is_solved()

0 commit comments

Comments
 (0)