44
55from __future__ import annotations
66
7+ from pulp import LpVariable , LpMinimize , LpProblem , lpSum , PULP_CBC_CMD , LpStatusOptimal , LpStatusNotSolved , value
8+
79from preflibtools .instances import OrdinalInstance
810from preflibtools .properties .subdomains .ordinal .singlecrossing import is_single_crossing
911
1012import itertools
1113
12- from mip import Model , CONTINUOUS , minimize , xsum , OptimizationStatus
13-
1414
1515def _append_to_axis (axis : list , a : int , b : int ):
1616 """Adds alternatives a and b to the axis in such a way that a is to the
@@ -59,7 +59,7 @@ def _restrict_preferences(instance: OrdinalInstance, C_set_plus: set):
5959
6060def _one_euclidean_solve_lp (preferences : list , axis : list ):
6161 """Attempts to solve the linear program for the 1-Euclidean domain,
62- given the preferences and the axis. Makes use of the MIP library.
62+ given the preferences and the axis. Makes use of the PuLP library.
6363
6464 Args:
6565 preferences (list): the preferences of the voters
@@ -79,69 +79,57 @@ def _one_euclidean_solve_lp(preferences: list, axis: list):
7979 n = len (preferences )
8080 m = len (axis )
8181
82- model = Model ( )
82+ model = LpProblem ( "1Euclidean-Model" , LpMinimize )
8383
84- # add variables for the voters and alternatives
85- all_vars = [model .add_var (var_type = CONTINUOUS , name = f"voter_{ i } " ) for i in range (n )]
86- all_vars += [
87- model .add_var (var_type = CONTINUOUS , name = f"alternative_{ axis [i ]} " )
88- for i in range (m )
89- ]
84+ # Variables: voter positions (continuous)
85+ voter_vars = [LpVariable (f"voter_{ i } " , cat = "Continuous" ) for i in range (n )]
9086
91- # TODO: var for axis.index(pair[0]) etc.
87+ # Variables: alternative positions (continuous), ordered by axis
88+ alt_vars = {
89+ alt_id : LpVariable (f"alternative_{ alt_id } " , cat = "Continuous" )
90+ for alt_id in axis
91+ }
92+
93+ # Bounding the vars
94+ model += alt_vars [axis [0 ]] == 0
95+ for v in alt_vars .values ():
96+ model += v >= 0
97+ model += v <= max (n , m ) + 2
98+ for v in voter_vars :
99+ model += v >= 0
100+ model += v <= max (n , m ) + 2
92101
93102 for pair in pairs :
94- # add axis constraints
95- # print("pair:", pair)
96- # model += vars[n + pair[0] - 1] + 1 <= vars[n + pair[1] - 1]
97- # print("n + axis.index(pair[0]) - 1: ", n + axis.index(pair[0]) - 1)
98- # print("n + axis.index(pair[1]) - 1: ", n + axis.index(pair[1]) - 1)
99- # print("vars[n + axis.index(pair[1]) - 1]: ", vars[n + axis.index(pair[1]) - 1])
100- model += (
101- all_vars [n + axis .index (pair [0 ])] + 1 <= all_vars [n + axis .index (pair [1 ])]
102- )
103+ a , b = pair
104+ # Constraint: alt_vars[a] + 1 <= alt_vars[b]
105+ model += alt_vars [a ] + 1 <= alt_vars [b ]
106+
103107 # add voter constraints
104108 for i in range (n ):
105- # if voter prefers a to b
106- if preferences [ i ] .index (pair [ 0 ] ) < preferences [ i ] .index (pair [ 1 ] ):
107- # model += vars[i] + 1 <= (vars[n + pair[0] - 1] + vars[n + pair[1] - 1]) / 2
109+ voter_pref = preferences [ i ]
110+ if voter_pref .index (a ) < voter_pref .index (b ):
111+ # voter i prefers a to b
108112 model += (
109- all_vars [i ] + 1
110- <= (
111- all_vars [n + axis .index (pair [0 ])]
112- + all_vars [n + axis .index (pair [1 ])]
113- )
114- / 2
113+ voter_vars [i ] + 1
114+ <= (alt_vars [a ] + alt_vars [b ]) / 2
115115 )
116116 else :
117- # model += vars[i] >= (vars[n + pair[1] - 1] + vars[n + pair[0] - 1]) / 2 + 1
117+ # voter i prefers b to a
118118 model += (
119- all_vars [i ]
120- >= (
121- all_vars [n + axis .index (pair [1 ])]
122- + all_vars [n + axis .index (pair [0 ])]
123- )
124- / 2
125- + 1
119+ voter_vars [i ]
120+ >= (alt_vars [a ] + alt_vars [b ]) / 2 + 1
126121 )
127122
128- model .objective = minimize (xsum (all_vars ))
129-
130- # suppress log
131- model .verbose = 0
132-
133- status = model .optimize ()
123+ # Objective: minimize sum of all variables (voter + alt positions)
124+ model += lpSum (voter_vars + list (alt_vars .values ()))
134125
135- if status == OptimizationStatus .OPTIMAL or status == OptimizationStatus .FEASIBLE :
136- alternatives = dict ()
137- for i in range (m ):
138- alternatives [axis [i ]] = all_vars [n + i ].x
139-
140- voters = []
141- for i in range (n ):
142- voters .append (all_vars [i ].x )
126+ # Solve the model
127+ model .solve (PULP_CBC_CMD (msg = False ))
143128
144- return status , voters , alternatives
129+ if model .status in [LpStatusOptimal , LpStatusNotSolved ]:
130+ alternatives = {alt_id : value (var ) for alt_id , var in alt_vars .items ()}
131+ voters = [value (var ) for var in voter_vars ]
132+ return model .status , voters , alternatives
145133
146134 return None , None , None
147135
@@ -341,7 +329,7 @@ def is_one_euclidean(instance: OrdinalInstance):
341329 tmp = voters + [alternatives [i ] for i in C_set_plus ]
342330
343331 # TODO: see previous comment, delta currently as value
344- delta = max ([abs (x - y ) for x , y in itertools .permutations (tmp , 2 )])
332+ delta = max ([abs (x - y ) for x , y in itertools .permutations (tmp , 2 )], default = 0 )
345333
346334 y = dict ()
347335 # add mapping of voters
0 commit comments