From b1143eb38e5427234a9cb8e8f16b58246b69ee79 Mon Sep 17 00:00:00 2001 From: Simon Rey Date: Tue, 2 Jul 2024 11:23:10 +0200 Subject: [PATCH] Fix problem with empty ballots when reading pb files --- pabutools/__init__.py | 2 +- pabutools/analysis/priceability.py | 3 +- pabutools/election/pabulib.py | 21 ++++---- pyproject.toml | 2 +- tests/test_pabulib.py | 79 ++++++++++++++++++++++++++++-- 5 files changed, 90 insertions(+), 17 deletions(-) diff --git a/pabutools/__init__.py b/pabutools/__init__.py index 4bb95259..0e2efa58 100644 --- a/pabutools/__init__.py +++ b/pabutools/__init__.py @@ -1,3 +1,3 @@ __author__ = "Simon Rey, Grzegorz Pierczyński, Markus Utke and Piotr Skowron" __email__ = "reysimon@orange.fr" -__version__ = "1.1.7" +__version__ = "1.1.8" diff --git a/pabutools/analysis/priceability.py b/pabutools/analysis/priceability.py index 819d44b3..1b03e2e3 100644 --- a/pabutools/analysis/priceability.py +++ b/pabutools/analysis/priceability.py @@ -390,10 +390,11 @@ def priceable( else: status = mip_model.optimize(max_seconds=max_seconds) - if status == OptimizationStatus.INF_OR_UNBD: + if hasattr(OptimizationStatus, "INF_OR_UNBD") and status == OptimizationStatus.INF_OR_UNBD: # https://support.gurobi.com/hc/en-us/articles/4402704428177-How-do-I-resolve-the-error-Model-is-infeasible-or-unbounded # https://github.com/coin-or/python-mip/blob/1.15.0/mip/gurobi.py#L777 # https://github.com/coin-or/python-mip/blob/1.16-pre/mip/gurobi.py#L778 + # This is not part of old python-mip version, hence the first test # mip_model.solver.set_int_param("DualReductions", 0) mip_model.reset() diff --git a/pabutools/election/pabulib.py b/pabutools/election/pabulib.py index c71f4204..46b7d11c 100644 --- a/pabutools/election/pabulib.py +++ b/pabutools/election/pabulib.py @@ -96,24 +96,27 @@ def parse_pabulib_from_string(file_content: str) -> tuple[Instance, Profile]: if vote_type == "approval": ballot = ApprovalBallot() for project_name in ballot_meta["vote"].split(","): - ballot.add(instance.get_project(project_name)) + if project_name: + ballot.add(instance.get_project(project_name)) ballot_meta.pop("vote") elif vote_type in ["scoring", "cumulative"]: if vote_type == "scoring": ballot = CardinalBallot() else: ballot = CumulativeBallot() - points = ballot_meta["points"].split(",") - for index, project_name in enumerate(ballot_meta["vote"].split(",")): - ballot[instance.get_project(project_name)] = str_as_frac( - points[index].strip() - ) - ballot_meta.pop("vote") - ballot_meta.pop("points") + if "points" in ballot_meta: # if not, the ballot should be empty + points = ballot_meta["points"].split(",") + for index, project_name in enumerate(ballot_meta["vote"].split(",")): + ballot[instance.get_project(project_name)] = str_as_frac( + points[index].strip() + ) + ballot_meta.pop("vote") + ballot_meta.pop("points") elif vote_type == "ordinal": ballot = OrdinalBallot() for project_name in ballot_meta["vote"].split(","): - ballot.append(instance.get_project(project_name)) + if project_name: + ballot.append(instance.get_project(project_name)) ballot_meta.pop("vote") else: raise NotImplementedError( diff --git a/pyproject.toml b/pyproject.toml index ab36781a..a2ccaa71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "pabutools" -version = "1.1.7" +version = "1.1.8" description = "Implementation of all the tools necessary to explore and analyse participatory budgeting elections" authors = [ { name = "Simon Rey", email = "reysimon@orange.fr" }, diff --git a/tests/test_pabulib.py b/tests/test_pabulib.py index 0a885e15..df2feac8 100644 --- a/tests/test_pabulib.py +++ b/tests/test_pabulib.py @@ -1,6 +1,6 @@ from unittest import TestCase -from pabutools.election import OrdinalBallot +from pabutools.election import OrdinalBallot, ApprovalBallot from pabutools.election.pabulib import ( parse_pabulib, parse_pabulib_from_string, @@ -93,6 +93,71 @@ def test_approval(self): os.remove("test.pb") + def test_empty_voters(self): + contents = """META +key;value +description;Auto-filled description +country;Auto-filled country +unit;Auto-filled unit +instance;Auto-filled instance +num_projects;20 +num_votes;1000 +budget;600000 +vote_type;approval +rule;Auto-filled rule +PROJECTS +project_id;cost +p0;124200 +p1;128400 +p2;135600 +p3;128400 +p4;316929 +p5;51599 +p6;66000 +p7;55200 +p8;55800 +p9;54000 +p10;66600 +p11;61199 +p12;51000 +p13;59400 +p14;59400 +p15;58800 +p16;63000 +p17;64800 +p18;66000 +p19;57600 +VOTES +voter_id;vote +0;p12,p19,p1,p14 +1;p11,p1,p13 +2;p16 +3;p12,p4,p9 +4;p0 +5;p12,p18,p16,p2 +6; +7;p6,p3,p13,p17,p19 +8;p5,p10 +9; +10;p0,p2 +11;p11,p6,p2 +12;p16,p19 +13; +14;p10,p2""" + + with open("test.pb", "w", encoding="utf-8") as f: + f.write(contents) + + for instance, profile in [ + parse_pabulib("test.pb"), + parse_pabulib_from_string(contents), + ]: + assert len(instance) == 20 + assert len(profile) == 15 + assert profile[6] == ApprovalBallot() + + os.remove("test.pb") + def test_cumulative(self): contents = """META key;value @@ -156,7 +221,9 @@ def test_cumulative(self): 8;14,8,2,4;3,2,1,1 9;1,6,7,9,10,20,30;1,1,1,1,1,1,1 10;6,7;3,3 -11;2,14,8;3,3,1""" +11;2,14,8;3,3,1 +12; +13;6,7;3,3""" with open("test.pb", "w", encoding="utf-8") as f: f.write(contents) @@ -167,7 +234,7 @@ def test_cumulative(self): ]: assert len(instance) == 30 assert instance.budget_limit == 1000000 - assert len(profile) == 12 + assert len(profile) == 14 assert len(profile[0]) == 4 assert len(profile[4]) == 5 @@ -247,7 +314,8 @@ def test_scoring(self): 8;14,8,2,4;3,2,1,1 9;1,6,7,9,10,20,30;1,1,1,1,1,1,1 10;6,7;3,3 -11;2,14,8;3,3,1""" +11;2,14,8;3,3,1 +12;""" with open("test.pb", "w", encoding="utf-8") as f: f.write(contents) @@ -452,6 +520,7 @@ def test_ordinal(self): 42;95,75,111;paper;PODGÓRZE DUCHACKIE 43;84,101,83;internet;PODGÓRZE 44;4,36,41;internet;KROWODRZA +45;;internet;KROWODRZA """ with open("test.pb", "w", encoding="utf-8") as f: @@ -463,7 +532,7 @@ def test_ordinal(self): ]: assert len(instance) == 123 assert instance.budget_limit == 8000100 - assert len(profile) == 45 + assert len(profile) == 46 assert len(profile[44]) == 3 assert len(profile[4]) == 3