Skip to content

Commit e2c6652

Browse files
committed
Switching to PuLP
1 parent 1d4c5ec commit e2c6652

7 files changed

Lines changed: 104 additions & 71 deletions

File tree

preflibtools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = "Simon Rey"
22
__email__ = "reysimon@orange.fr"
3-
__version__ = "2.0.32"
3+
__version__ = "2.0.33"

preflibtools/properties/subdomains/ordinal/euclidean.py

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
from __future__ import annotations
66

7+
from pulp import LpVariable, LpMinimize, LpProblem, lpSum, PULP_CBC_CMD, LpStatusOptimal, LpStatusNotSolved, value
8+
79
from preflibtools.instances import OrdinalInstance
810
from preflibtools.properties.subdomains.ordinal.singlecrossing import is_single_crossing
911

1012
import itertools
1113

12-
from mip import Model, CONTINUOUS, minimize, xsum, OptimizationStatus
13-
1414

1515
def _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

6060
def _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

preflibtools/properties/subdomains/ordinal/singlepeaked/singlepeakedness.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from itertools import combinations
1010

1111
import numpy as np
12-
from mip import Model, xsum, BINARY, MINIMIZE, INTEGER, OptimizationStatus
1312

1413
from preflibtools.properties.subdomains.consecutive_ones import isC1P
1514

@@ -567,6 +566,15 @@ def is_single_peaked_ILP(instance):
567566
:rtype: Tuple(bool, str, list)
568567
"""
569568

569+
try:
570+
from mip import Model, MINIMIZE, BINARY, INTEGER, OptimizationStatus
571+
except ImportError:
572+
raise ImportError(
573+
"The Single-Peaked ILP functions needs the python-mip package. This is no longer listed as a requirement "
574+
"as we are slowly moving to PuLP for Python 3.13 compatibility. If you are using Python < 3.13, install "
575+
"manually the python-mip package. If you are using Python 3.13, please help us translate this file to PuLP."
576+
)
577+
570578
if instance.data_type not in ("toc", "soc"):
571579
raise TypeError(
572580
"You are trying to test for single-peakedness on an instance of type "
@@ -669,6 +677,15 @@ def approx_SP_voter_deletion_ILP(instance, weighted=False):
669677
+ ", this is not possible. Only toc and soc are allowed here."
670678
)
671679

680+
try:
681+
from mip import Model, MINIMIZE, BINARY, INTEGER, OptimizationStatus, xsum
682+
except ImportError:
683+
raise ImportError(
684+
"The Single-Peaked ILP functions needs the python-mip package. This is no longer listed as a requirement "
685+
"as we are slowly moving to PuLP for Python 3.13 compatibility. If you are using Python < 3.13, install "
686+
"manually the python-mip package. If you are using Python 3.13, please help us translate this file to PuLP."
687+
)
688+
672689
model = Model(sense=MINIMIZE)
673690

674691
init_time = time.time()
@@ -784,6 +801,15 @@ def approx_SP_alternative_deletion_ILP(instance):
784801
+ ", this is not possible. Only toc and soc are allowed here."
785802
)
786803

804+
try:
805+
from mip import Model, MINIMIZE, BINARY, INTEGER, OptimizationStatus, xsum
806+
except ImportError:
807+
raise ImportError(
808+
"The Single-Peaked ILP functions needs the python-mip package. This is no longer listed as a requirement "
809+
"as we are slowly moving to PuLP for Python 3.13 compatibility. If you are using Python < 3.13, install "
810+
"manually the python-mip package. If you are using Python 3.13, please help us translate this file to PuLP."
811+
)
812+
787813
model = Model(sense=MINIMIZE)
788814

789815
init_time = time.time()

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
44

55
[project]
66
name = "preflibtools"
7-
version = "2.0.32"
7+
version = "2.0.33"
88
description = "A set of tools to work with preference data from the PrefLib.org website."
99
authors = [
1010
{ name = "Simon Rey", email = "reysimon@orange.fr" },
@@ -21,11 +21,12 @@ classifiers = [
2121
"Programming Language :: Python :: 3.10",
2222
"Programming Language :: Python :: 3.11",
2323
"Programming Language :: Python :: 3.12",
24+
"Programming Language :: Python :: 3.13",
2425
]
2526
requires-python = ">=3.7"
2627
dependencies = [
2728
"numpy",
28-
"mip",
29+
"pulp",
2930
"prefsampling>=0.1.7"
3031
]
3132

tests/instance/io/test_io_cat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_read_from_str(self):
5454
def test_read_from_url(self):
5555
instance = CategoricalInstance()
5656
instance.parse_url(
57-
"https://www.preflib.org/static/data/aamas/00037-00000001.cat"
57+
"https://raw.githubusercontent.com/PrefLib/PrefLib-Data/main/datasets/00037%20-%20aamas/00037-00000001.cat"
5858
)
5959

6060
def test_read_autocorrect(self):

tests/instance/io/test_io_soi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_read_from_str(self):
4040

4141
def test_read_from_url(self):
4242
instance = OrdinalInstance()
43-
instance.parse_url("https://www.preflib.org/static/data/apa/00028-00000007.soi")
43+
instance.parse_url("https://raw.githubusercontent.com/PrefLib/PrefLib-Data/main/datasets/00028%20-%20apa/00028-00000007.soi")
4444

4545
def test_autocorrect(self):
4646
instance = OrdinalInstance()

tests/properties/subdomains/ordinal/test_singlepeakedness.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ def test_single_peakedness(self):
2323
assert is_single_peaked_axis(instance, [0, 1, 2]) is False
2424
assert is_single_peaked_axis(instance, [1, 0, 2]) is True
2525
assert is_single_peaked(instance)[0] is True
26-
assert is_single_peaked_ILP(instance)[0] is True
26+
try:
27+
assert is_single_peaked_ILP(instance)[0] is True
28+
except ImportError:
29+
pass
2730
assert is_single_peaked_pq_tree(instance) is True
2831

2932
instance = OrdinalInstance()
@@ -38,18 +41,27 @@ def test_single_peakedness(self):
3841
assert is_single_peaked_axis(instance, [1, 0, 2]) is False
3942
assert is_single_peaked(instance)[0] is True
4043
assert is_single_peaked(instance)[1] in ([0, 1, 2], [2, 1, 0])
41-
assert is_single_peaked_ILP(instance)[0] is True
42-
assert is_single_peaked_ILP(instance)[2] in ([0, 1, 2], [2, 1, 0])
44+
45+
try:
46+
assert is_single_peaked_ILP(instance)[0] is True
47+
assert is_single_peaked_ILP(instance)[2] in ([0, 1, 2], [2, 1, 0])
48+
except ImportError:
49+
pass
4350
assert is_single_peaked_pq_tree(instance) is True
4451

4552
instance = OrdinalInstance()
4653
orders = [((0,), (1,), (2,)), ((2,), (0,), (1,)), ((1,), (2,), (0,))]
4754
instance.append_order_list(orders)
4855
assert is_single_peaked(instance)[0] is False
49-
assert is_single_peaked_ILP(instance)[0] is False
56+
try:
57+
assert is_single_peaked_ILP(instance)[0] is False
58+
assert approx_SP_voter_deletion_ILP(instance)[0] == 1
59+
assert approx_SP_alternative_deletion_ILP(instance)[0] == 1
60+
assert approx_SP_voter_deletion_ILP(instance)[0] == 1
61+
assert approx_SP_alternative_deletion_ILP(instance)[0] == 1
62+
except ImportError:
63+
pass
5064
assert is_single_peaked_pq_tree(instance) is False
51-
assert approx_SP_voter_deletion_ILP(instance)[0] == 1
52-
assert approx_SP_alternative_deletion_ILP(instance)[0] == 1
5365

5466
instance = OrdinalInstance()
5567
orders = [
@@ -58,16 +70,22 @@ def test_single_peakedness(self):
5870
((2, 3), (1,), (0,), (4,)),
5971
]
6072
instance.append_order_list(orders)
61-
assert is_single_peaked_ILP(instance)[0] is True
73+
try:
74+
assert is_single_peaked_ILP(instance)[0] is True
75+
assert approx_SP_voter_deletion_ILP(instance)[0] == 0
76+
assert approx_SP_alternative_deletion_ILP(instance)[0] == 0
77+
except ImportError:
78+
pass
6279
assert is_single_peaked_pq_tree(instance) is True
63-
assert approx_SP_voter_deletion_ILP(instance)[0] == 0
64-
assert approx_SP_alternative_deletion_ILP(instance)[0] == 0
6580

6681
instance = OrdinalInstance()
6782
instance.populate_mallows_mix(30, 7, 5)
68-
is_single_peaked_ILP(instance)
69-
approx_SP_voter_deletion_ILP(instance)
70-
approx_SP_alternative_deletion_ILP(instance)
83+
try:
84+
is_single_peaked_ILP(instance)
85+
approx_SP_voter_deletion_ILP(instance)
86+
approx_SP_alternative_deletion_ILP(instance)
87+
except ImportError:
88+
pass
7189

7290
def test_single_peakedness_prefsampling(self):
7391
for seed in range(50):

0 commit comments

Comments
 (0)