diff --git a/open_spiel/python/voting/preflib_util.py b/open_spiel/python/voting/preflib_util.py new file mode 100644 index 0000000000..f1fe118b98 --- /dev/null +++ b/open_spiel/python/voting/preflib_util.py @@ -0,0 +1,79 @@ +# Copyright 2023 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers to work with PrefLib data.""" + +import pyspiel +from open_spiel.python.voting import base + + +def parse_preflib_data(string_data: str) -> base.PreferenceProfile: + """Parses the contents of a PrefLib data file. + + Currently only supports SOC and SOI. See https://www.preflib.org/format. + + Args: + string_data: the name of the file to parse. + + Returns: + A preference profile. + """ + lines = string_data.split("\n") + alternatives = [] + num_alternatives = None + num_votes = None + profile = base.PreferenceProfile() + for raw_line in lines: + line = raw_line.strip() + if not line: continue + if line.startswith("#"): + parts = line.split(" ") + if line.startswith("# DATA TYPE: "): + assert(parts[3] == "soc" or parts[3] == "soi") + elif line.startswith("# NUMBER ALTERNATIVES:"): + num_alternatives = int(parts[3]) + alternatives = [None] * num_alternatives + elif line.startswith("# NUMBER VOTERS:"): + num_votes = int(parts[3]) + elif line.startswith("# ALTERNATIVE NAME "): + num = int(parts[3].split(":")[0]) + index_of_colon = line.index(":") + assert 1 <= num <= num_alternatives + alternatives[num-1] = line[index_of_colon+2:] + else: + if profile.num_alternatives() == 0: + profile = base.PreferenceProfile(alternatives=alternatives) + index_of_colon = line.index(":") + weight = int(line[:index_of_colon]) + vote_parts = line[index_of_colon+2:].split(",") + vote = [alternatives[int(part) - 1] for part in vote_parts] + if weight > 0: + profile.add_vote(vote, weight) + assert num_votes == profile.num_votes() + return profile + + +def parse_preflib_datafile(filename: str) -> base.PreferenceProfile: + """Parses a Preflib data file. + + Currently only supports SOC and SOI. See https://www.preflib.org/format. + + Args: + filename: the name of the file to parse. + + Returns: + A preference profile. + """ + contents = pyspiel.read_contents_from_file(filename, "r") + return parse_preflib_data(contents) diff --git a/open_spiel/python/voting/preflib_util_test.py b/open_spiel/python/voting/preflib_util_test.py new file mode 100644 index 0000000000..bc967ad9d5 --- /dev/null +++ b/open_spiel/python/voting/preflib_util_test.py @@ -0,0 +1,60 @@ +# Copyright 2023 DeepMind Technologies Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for open_spiel.python.voting.util.""" + +from absl.testing import absltest +from open_spiel.python.voting import preflib_util + +TEST_DATA = """ +# FILE NAME: 00004-00000050.soc +# TITLE: Netflix Prize Data +# DESCRIPTION: +# DATA TYPE: soc +# MODIFICATION TYPE: induced +# RELATES TO: +# RELATED FILES: +# PUBLICATION DATE: 2013-08-17 +# MODIFICATION DATE: 2022-09-16 +# NUMBER ALTERNATIVES: 3 +# NUMBER VOTERS: 391 +# NUMBER UNIQUE ORDERS: 6 +# ALTERNATIVE NAME 1: The Amityville Horror +# ALTERNATIVE NAME 2: Mars Attacks! +# ALTERNATIVE NAME 3: Lean on Me +186: 3,1,2 +71: 1,3,2 +58: 3,2,1 +45: 2,3,1 +18: 1,2,3 +13: 2,1,3 +""" + + +class UtilTest(absltest.TestCase): + def test_load_preflib(self): + print(TEST_DATA) + profile = preflib_util.parse_preflib_data(TEST_DATA) + print(profile) + self.assertEqual(profile.num_alternatives(), 3) + self.assertEqual(profile.num_votes(), 391) + self.assertListEqual(profile.alternatives, [ + "The Amityville Horror", "Mars Attacks!", "Lean on Me" + ]) + print(profile.alternatives) + print(profile.margin_matrix()) + + +if __name__ == "__main__": + absltest.main()