99 LpBinary ,
1010 LpVariable ,
1111 LpInteger ,
12- HiGHS ,
12+ HiGHS , LpAffineExpression ,
1313)
1414
1515from trivoting .election import AbstractTrichotomousProfile , Selection
16+ from trivoting .rules .ilp_schemes import ILPBUilder , ilp_optimiser_rule
1617from trivoting .utils import generate_subsets
1718
1819
@@ -38,6 +39,35 @@ def chamberlin_courant_brute_force(
3839 return arg_max_coverage [0 ]
3940 return arg_max_coverage
4041
42+ class ChamberlinCourantILPBuilder (ILPBUilder ):
43+ model_name = "ChamberlinCourant"
44+
45+ def init_vars (self ) -> None :
46+ super (ChamberlinCourantILPBuilder , self ).init_vars ()
47+
48+ self .vars ["sat_var" ] = dict ()
49+ self .vars ["dissat_var" ] = dict ()
50+ self .vars ["cc_var" ] = dict ()
51+
52+ for i , ballot in enumerate (self .profile ):
53+ sat_var = LpVariable (f"sat_{ i } " , lowBound = - self .max_size_selection , upBound = self .max_size_selection , cat = LpInteger )
54+ dissat_var = LpVariable (f"dissat_{ i } " , lowBound = - self .max_size_selection , upBound = self .max_size_selection , cat = LpInteger )
55+ cc_var = LpVariable (f"cc_{ i } " , cat = LpBinary )
56+
57+ self .model += sat_var == lpSum (self .vars ["selection" ][alt ] for alt in ballot .approved )
58+ self .model += dissat_var == lpSum (self .vars ["selection" ][alt ] for alt in ballot .disapproved )
59+
60+ # Linearisation of z = 1 if x > y and z = 0 otherwise
61+ self .model += (sat_var - dissat_var >= 1 - (1 - cc_var ) * self .max_size_selection )
62+ self .model += (sat_var - dissat_var <= cc_var * self .max_size_selection )
63+
64+ self .vars ["sat_var" ][i ] = sat_var
65+ self .vars ["dissat_var" ][i ] = dissat_var
66+ self .vars ["cc_var" ][i ] = cc_var
67+
68+ def objective (self ) -> LpAffineExpression :
69+ return lpSum (self .vars ["cc_var" ].values ())
70+
4171
4272def chamberlin_courant (
4373 profile : AbstractTrichotomousProfile ,
@@ -75,114 +105,5 @@ def chamberlin_courant(
75105 The selection if resolute (:code:`resoluteness == True`), or a list of selections
76106 if irresolute (:code:`resoluteness == False`).
77107 """
78- model = LpProblem ("ChamberlinCourant" , LpMaximize )
79-
80- selection_vars = {
81- alt : LpVariable (f"y_{ alt .name } " , cat = LpBinary ) for alt in profile .alternatives
82- }
83-
84- # Select no more than allowed
85- model += lpSum (selection_vars .values ()) <= max_size_selection
86-
87- # Handle initial selection
88- if initial_selection is not None :
89- for alt in initial_selection .selected :
90- model += selection_vars [alt ] == 1
91- if not initial_selection .implicit_reject :
92- for alt in initial_selection .rejected :
93- model += selection_vars [alt ] == 0
94-
95- # Voters satisfaction variables
96- voter_details = []
97- for i , ballot in enumerate (profile ):
98- voter = {
99- "ballot" : ballot ,
100- "multiplicity" : profile .multiplicity (ballot ),
101- "sat_var" : LpVariable (
102- f"sat_{ i } " , lowBound = - max_size_selection , upBound = max_size_selection , cat = LpInteger
103- ),
104- "dissat_var" : LpVariable (
105- f"disat_{ i } " , lowBound = - max_size_selection , upBound = max_size_selection , cat = LpInteger
106- ),
107- "cc_var" : LpVariable (f"cc_{ i } " , cat = LpBinary ),
108- }
109- voter_details .append (voter )
110-
111- # Constraint voter_details to ensure proper counting
112- for voter in voter_details :
113- model += voter ["sat_var" ] == lpSum (
114- selection_vars [alt ] for alt in voter ["ballot" ].approved
115- )
116- model += voter ["dissat_var" ] == lpSum (
117- selection_vars [alt ] for alt in voter ["ballot" ].disapproved
118- )
119-
120- # Linearisation of z = 1 if x > y and z = 0 otherwise
121- model += (
122- voter ["sat_var" ] - voter ["dissat_var" ]
123- >= 1 - (1 - voter ["cc_var" ]) * max_size_selection
124- )
125- model += (
126- voter ["sat_var" ] - voter ["dissat_var" ]
127- <= voter ["cc_var" ] * max_size_selection
128- )
129-
130- # Objective: max PAV score
131- model += lpSum (voter ["cc_var" ] for voter in voter_details )
132-
133- status = model .solve (HiGHS (msg = verbose , timeLimit = max_seconds ))
134-
135- all_selections = []
136-
137- if status == LpStatusOptimal :
138- selection = Selection (implicit_reject = True )
139- for alt , v in selection_vars .items ():
140- if value (v ) >= 0.9 :
141- selection .add_selected (alt )
142- all_selections .append (selection )
143- else :
144- raise ValueError (
145- f"Solver did not find an optimal solution, status is { status } ."
146- )
147-
148- if resoluteness :
149- return all_selections [0 ]
150-
151- # If irresolute, we solve again, banning the previous selections
152- model += lpSum (voter ["cc_var" ] for voter in voter_details ) == value (model .objective )
153-
154- previous_selection = selection
155- while True :
156- # See http://yetanothermathprogrammingconsultant.blogspot.com/2011/10/integer-cuts.html
157- model += (
158- lpSum ((1 - selection_vars [a ]) for a in previous_selection .selected )
159- + lpSum (
160- selection_vars [a ] for a in selection_vars if a not in previous_selection
161- )
162- ) >= 1
163-
164- model += (
165- lpSum (selection_vars [a ] for a in previous_selection .selected )
166- - lpSum (
167- selection_vars [a ] for a in selection_vars if a not in previous_selection
168- )
169- ) <= len (previous_selection ) - 1
170-
171- status = model .solve (HiGHS (msg = verbose , timeLimit = max_seconds ))
172-
173- if status != LpStatusOptimal :
174- break
175-
176- previous_selection = Selection (
177- [
178- a
179- for a in selection_vars
180- if value (selection_vars [a ]) is not None
181- and value (selection_vars [a ]) >= 0.9
182- ],
183- implicit_reject = True ,
184- )
185- if previous_selection not in all_selections :
186- all_selections .append (previous_selection )
187-
188- return all_selections
108+ ilp_builder = ChamberlinCourantILPBuilder (profile , max_size_selection , initial_selection , max_seconds = max_seconds , verbose = verbose )
109+ return ilp_optimiser_rule (ilp_builder , resoluteness = resoluteness )
0 commit comments