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