Skip to content

Commit

Permalink
add parallel version of split_cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
epacuit committed May 10, 2024
1 parent 592065c commit 842e037
Show file tree
Hide file tree
Showing 9 changed files with 738 additions and 14 deletions.
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
author = 'Wes Holliday and Eric Pacuit'

# The full version, including alpha/beta/rc tags
release = '1.3.0'
release = '1.3.2'


# -- General configuration ---------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion pref_voting/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.3.0'
__version__ = '1.3.2'
3 changes: 3 additions & 0 deletions pref_voting/iterative_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,13 @@ def instant_runoff(profile, curr_cands = None, algorithm = "basic"):
# Create some aliases for instant runoff
instant_runoff.set_name("Hare")
hare = copy.deepcopy(instant_runoff)
hare.skip_registration = True
instant_runoff.set_name("Ranked Choice")
ranked_choice = copy.deepcopy(instant_runoff)
ranked_choice.skip_registration = True
instant_runoff.set_name("Alternative Vote")
alternative_vote = copy.deepcopy(instant_runoff)
alternative_vote.skip_registration = True

# reset the name Instant Runoff
instant_runoff.set_name("Instant Runoff")
Expand Down
51 changes: 48 additions & 3 deletions pref_voting/margin_based_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
'''

from pref_voting.voting_method import *
from pref_voting.c1_methods import gocha
from pref_voting.weighted_majority_graphs import MajorityGraph, MarginGraph
from pref_voting.probabilistic_methods import maximal_lottery, c1_maximal_lottery
from pref_voting.helper import get_mg, SPO
import math
from itertools import product, permutations, combinations, chain
import networkx as nx
from pref_voting.voting_method_properties import VotingMethodProperties, ElectionTypes
import multiprocessing as mp
from multiprocessing import Pool
from functools import partial

minimax_properties = VotingMethodProperties(
condorcet_winner=True,
Expand Down Expand Up @@ -233,6 +235,7 @@ def _schwartz_sequential_dropping(edata, curr_cands = None, strength_function =
Returns:
A sorted list of candidates.
"""
from pref_voting.c1_methods import gocha

strength_function = edata.margin if strength_function is None else strength_function

Expand Down Expand Up @@ -401,7 +404,7 @@ def _split_cycle_basic(
edata,
curr_cands = None,
strength_function = None):
"""An implementation of Split Cycle based on the mathematical definition. This is more efficient than the floyd_warshall implementation.
"""An implementation of Split Cycle based on the mathematical definition.
"""
strength_matrix, cand_to_cindex = edata.strength_matrix(curr_cands = curr_cands, strength_function=strength_function)

Expand All @@ -419,6 +422,41 @@ def _split_cycle_basic(

return sorted(potential_winners)

def _is_cand_split_cycle_defeated(a, strength_matrix):

for b in range(strength_matrix.shape[0]):
if strength_matrix[b][a] > strength_matrix[a][b] and not has_strong_path(strength_matrix, a, b, strength_matrix[b][a]):
return True
return False


def batch(iterable, n=1):
l = len(iterable)
for ndx in range(0, l, n):
yield iterable[ndx:min(ndx + n, l)]

def process_batch_of_candidates(batch, strength_matrix):
results = []
for candidate in batch:
result = _is_cand_split_cycle_defeated(candidate, strength_matrix)
results.append(result)
return results

def _split_cycle_basic_parallel(strength_matrix, num_cpus=4):

num_cands = strength_matrix.shape[0]
cands = list(range(num_cands))
batch_size = num_cands // num_cpus + (num_cands % num_cpus > 0)
candidate_batches = list(batch(cands, batch_size))
with Pool(num_cpus) as pool:
batch_args = [(batch, strength_matrix)
for batch in candidate_batches]
results = pool.starmap(process_batch_of_candidates, batch_args)
# Flatten the list of results
sc_defeat_data = [item for sublist in results for item in sublist]

return sorted([c for c in cands if not sc_defeat_data[c]])

def _split_cycle_floyd_warshall(
edata,
curr_cands = None,
Expand Down Expand Up @@ -471,7 +509,8 @@ def split_cycle(
edata,
curr_cands=None,
strength_function=None,
algorithm='basic'):
algorithm='basic',
num_cpus=4):

"""A **majority cycle** is a sequence :math:`x_1, \ldots ,x_n` of distinct candidates with :math:`x_1=x_n` such that for :math:`1 \leq k \leq n-1`, :math:`x_k` is majority preferred to :math:`x_{k+1}`. The Split Cycle winners are determined as follows:
Expand Down Expand Up @@ -524,6 +563,12 @@ def split_cycle(
return _split_cycle_basic(edata, curr_cands = curr_cands, strength_function = strength_function)
elif algorithm == 'floyd_warshall':
return _split_cycle_floyd_warshall(edata, curr_cands = curr_cands, strength_function = strength_function)
elif algorithm == 'basic_parallel':
curr_cands = edata.candidates if curr_cands is None else curr_cands
strength_matrix, cand_to_cindex = edata.strength_matrix(curr_cands = curr_cands, strength_function=strength_function)
cindx_to_cand = {cand_to_cindex(c):c for c in curr_cands}
sc_ws = _split_cycle_basic_parallel(strength_matrix,num_cpus=num_cpus)
return sorted([cindx_to_cand[c] for c in sc_ws])
else:
raise ValueError("Invalid algorithm specified.")

Expand Down
27 changes: 25 additions & 2 deletions pref_voting/voting_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'''

import functools
import inspect
import numpy as np
from numba import jit # Remove until numba supports python 3.11
import random
Expand Down Expand Up @@ -35,13 +36,35 @@ def __init__(self,
self.properties = properties
self.input_types = input_types
self.skip_registration = skip_registration
self.algorithm = None

functools.update_wrapper(self, vm)

def __call__(self, edata, curr_cands = None, **kwargs):

if (curr_cands is not None and len(curr_cands) == 0) or len(edata.candidates) == 0:
return []
return self.vm(edata, curr_cands = curr_cands, **kwargs)

# Set the algorithm from self.algorithm if it's not already provided in kwargs
if 'algorithm' not in kwargs and self.algorithm is not None:
params = inspect.signature(self.vm).parameters
if 'algorithm' in params and params['algorithm'].kind in [inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD]:
kwargs['algorithm'] = self.algorithm

return self.vm(edata, curr_cands=curr_cands, **kwargs)

def set_algorithm(self, algorithm):
"""
Set the algorithm for the voting method if 'algorithm' is an accepted keyword parameter.
Args:
algorithm: The algorithm to set for the voting method.
"""
params = inspect.signature(self.vm).parameters
if 'algorithm' in params and params['algorithm'].kind in [inspect.Parameter.KEYWORD_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD]:
self.algorithm = algorithm
else:
raise ValueError(f"The method {self.name} does not accept 'algorithm' as a parameter.")

def choose(self, edata, curr_cands = None):
"""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"

[tool.poetry]
name = "pref_voting"
version = "1.3.0"
version = "1.3.2"
description = "pref_voting is a Python package that contains tools to reason about elections and margin graphs, and implementations of voting methods."
authors = ["Eric Pacuit <epacuit@umd.edu>"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="pref_voting",
version="1.3.0",
version="1.3.2",
author="Eric Pacuit",
author_email='epacuit@umd.edu',
description="pref_voting is a Python packaging that contains tools to reason about election profiles and margin graphs, and implementations of a variety of preferential voting methods.",
Expand Down
Loading

0 comments on commit 842e037

Please sign in to comment.