Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tests/test_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ def test_cav_basic(tiebreaker):
assert combined_approval(election, tiebreaker) == 1 # Nashville


def test_combined_approval_balanced_opposing_ballots_tie():
election = np.array([[1, -1], [-1, 1]], dtype=np.int8)
assert combined_approval(election) is None
assert combined_approval(election, tiebreaker='order') == 0


def combined_approval_ballots(min_cands=1, max_cands=25, min_voters=1,
max_voters=100):
"""
Expand Down
13 changes: 12 additions & 1 deletion tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from hypothesis import given
from hypothesis.strategies import integers, lists, permutations

from elsim.methods import black
from elsim.methods import black, borda, condorcet


@pytest.mark.parametrize("tiebreaker", [None, 'random', 'order'])
Expand Down Expand Up @@ -150,6 +150,17 @@ def test_legit_winner_none(election):
assert winner in set(range(n_cands)) | {None}


@given(election=complete_ranked_ballots(min_cands=2, max_cands=20,
min_voters=1, max_voters=80))
def test_black_equals_condorcet_or_borda(election):
election = np.asarray(election)
cw = condorcet(election)
if cw is not None:
assert black(election) == cw
else:
assert black(election) == borda(election)


if __name__ == "__main__":
# Run unit tests, in separate process to avoid warnings about cached
# modules, printing output line by line in realtime
Expand Down
26 changes: 26 additions & 0 deletions tests/test_condorcet.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,32 @@ def test_ranked_election_to_matrix(election):
assert_array_equal(np.diagonal(matrix), 0)


@given(election=complete_ranked_ballots(min_cands=2, max_cands=25,
min_voters=1, max_voters=100))
def test_pairwise_counts_partition_voters(election):
election = np.asarray(election)
matrix = ranked_election_to_matrix(election)
n_cands = matrix.shape[0]
n_voters = election.shape[0]
for i in range(n_cands):
for j in range(i + 1, n_cands):
assert matrix[i, j] + matrix[j, i] == n_voters


@given(election=complete_ranked_ballots(min_cands=2, max_cands=25,
min_voters=1, max_voters=100))
def test_condorcet_winner_beats_everyone_pairwise(election):
election = np.asarray(election)
winner = condorcet(election)
if winner is None:
return
matrix = ranked_election_to_matrix(election)
for opponent in range(matrix.shape[0]):
if opponent == winner:
continue
assert matrix[winner, opponent] > matrix[opponent, winner]


if __name__ == "__main__":
# Run unit tests, in separate process to avoid warnings about cached
# modules, printing output line by line in realtime
Expand Down
109 changes: 104 additions & 5 deletions tests/test_elections.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import numpy as np
import pytest
from hypothesis import assume, given
from hypothesis.extra.numpy import arrays
from hypothesis.strategies import floats, integers, tuples
from numpy.testing import (assert_allclose, assert_array_equal,
assert_array_less)

Expand All @@ -10,17 +13,19 @@
def test_random_utilities():
rng = np.random.default_rng(1788) # Deterministic tests

for n_voters in (1, 5, 100):
for n_cands in (1, 2, 3, 7, 100):
for n_voters in (0, 1, 5, 100):
for n_cands in (0, 1, 2, 3, 7, 100):
election = random_utilities(n_voters, n_cands, random_state=rng)

# Make sure rows are voters and columns are candidates
assert election.shape == (n_voters, n_cands)

# Make sure each row is within [0, 1]
# Make sure each row is within [0, 1) (``Generator.random``)
if n_cands == 0:
continue
for row in election:
assert row.min() >= 0
assert row.max() <= 1
assert row.max() < 1

# Check that utilities are equally distributed from 0 to 1
n_voters = 10000
Expand All @@ -43,8 +48,9 @@ def test_impartial_culture():
assert election.shape == (n_voters, n_cands)

# Make sure each row is a permutation with no ties
if n_cands == 0:
continue
for row in election:
# (Empty arrays test equal, which is fine)
assert_array_equal(np.bincount(row), 1)

# Check that rankings are equally distributed
Expand Down Expand Up @@ -83,6 +89,18 @@ def test_normal_electorate():
assert_allclose(np.std(voters[:, 1])*disp,
np.std(cands[:, 1]), rtol=0.1)

# Smaller draw: alternate correlation sign, 3D, n_voters or n_cands zero
voters_s, cands_s = normal_electorate(80, 40, 3, -0.2, 1.5,
random_state=rng)
assert voters_s.shape == (80, 3)
assert cands_s.shape == (40, 3)
assert np.isfinite(voters_s).all() and np.isfinite(cands_s).all()

voters_z, cands_z = normal_electorate(0, 5, 2, 0.0, 1.0,
random_state=rng)
assert voters_z.shape == (0, 2)
assert cands_z.shape == (5, 2)


def test_normed_dist_utilities():
voters = [[1, 1],
Expand All @@ -101,6 +119,12 @@ def test_normed_dist_utilities():
[0.762689671355725, 0.000000000000000, 1.000000000000000]]
)

# Two candidates at the same point: per-voter min == max after shifting,
# so normalization divides 0 by 0 and the row is all NaN.
voters_coinc = np.array([[0.0, 0.0]])
cands_coinc = np.array([[1.0, 0.0], [1.0, 0.0]])
assert np.isnan(normed_dist_utilities(voters_coinc, cands_coinc)).all()


@pytest.mark.parametrize("func", [random_utilities, impartial_culture,
normal_electorate])
Expand Down Expand Up @@ -132,6 +156,81 @@ def test_invalid_random_state(func):
func(5, 5, random_state='bananas')


@given(n_voters=integers(0, 60), n_cands=integers(0, 60))
def test_random_utilities_hypothesis_shape_and_range(n_voters, n_cands):
rng = np.random.default_rng(2026)
election = random_utilities(n_voters, n_cands, random_state=rng)
assert election.shape == (n_voters, n_cands)
if n_voters and n_cands:
assert (election >= 0).all()
assert (election < 1).all()


@given(n_voters=integers(0, 60), n_cands=integers(0, 60))
def test_impartial_culture_hypothesis_valid_rankings(n_voters, n_cands):
rng = np.random.default_rng(2027)
election = impartial_culture(n_voters, n_cands, random_state=rng)
assert election.shape == (n_voters, n_cands)
if n_cands == 0:
return
for row in election:
assert_array_equal(
np.bincount(row, minlength=n_cands), np.ones(n_cands, dtype=int)
)


@given(
n_voters=integers(0, 40),
n_cands=integers(0, 40),
dims=integers(1, 6),
corr=floats(-0.99, 0.99, allow_nan=False, allow_infinity=False),
disp=floats(0.01, 4.0, allow_nan=False, allow_infinity=False),
)
def test_normal_electorate_hypothesis_shapes_finite(
n_voters, n_cands, dims, corr, disp
):
assume(corr < 1 - 1e-9)
if dims > 1:
assume(corr > -1.0 / (dims - 1) + 1e-12)
rng = np.random.default_rng(2028)
voters, cands = normal_electorate(
n_voters, n_cands, dims, corr, disp, random_state=rng
)
assert voters.shape == (n_voters, dims)
assert cands.shape == (n_cands, dims)
assert np.isfinite(voters).all()
assert np.isfinite(cands).all()


@given(
voters=arrays(
np.float64,
tuples(integers(1, 12), integers(1, 4)),
elements=floats(-50, 50, allow_nan=False, allow_infinity=False),
),
cands=arrays(
np.float64,
tuples(integers(2, 12), integers(1, 4)),
elements=floats(-50, 50, allow_nan=False, allow_infinity=False),
),
)
def test_normed_dist_utilities_hypothesis_bounds_when_spread(
voters, cands,
):
assume(voters.shape[1] == cands.shape[1])
dists = np.linalg.norm(
voters[:, np.newaxis, :] - cands[np.newaxis, :, :], axis=-1
)
assume(np.all(np.ptp(dists, axis=1) > 1e-9))
utilities = normed_dist_utilities(voters, cands)
assert utilities.shape == (voters.shape[0], cands.shape[0])
assert np.isfinite(utilities).all()
assert (utilities >= 0).all()
assert (utilities <= 1).all()
assert_array_equal(utilities.min(axis=1), np.zeros(voters.shape[0]))
assert_array_equal(utilities.max(axis=1), np.ones(voters.shape[0]))


if __name__ == "__main__":
# Run unit tests, in separate process to avoid warnings about cached
# modules, printing output line by line in realtime
Expand Down
37 changes: 37 additions & 0 deletions tests/test_methods.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import numpy as np
import pytest
from hypothesis import given
from hypothesis.strategies import integers, lists, permutations

from elsim.methods import (approval, black, borda, combined_approval, coombs,
fptp, irv, runoff, score, utility_winner)
Expand Down Expand Up @@ -35,6 +38,40 @@ def test_ranked_method_unanimity(method):
assert method(election, 'order') == 3


def complete_ranked_ballots(min_cands=2, max_cands=25, min_voters=1,
max_voters=100):
n_cands = integers(min_value=min_cands, max_value=max_cands)
return n_cands.flatmap(lambda n: lists(permutations(range(n)),
min_size=min_voters,
max_size=max_voters))


@pytest.mark.parametrize(
"method",
[black, borda, fptp, runoff, irv, coombs],
)
@given(election=complete_ranked_ballots(min_cands=2, max_cands=15,
min_voters=1, max_voters=60))
def test_ranked_methods_order_tiebreak_returns_candidate_id(method, election):
election = np.asarray(election)
winner = method(election, tiebreaker='order')
n_cands = election.shape[1]
assert winner in set(range(n_cands))


@pytest.mark.parametrize(
"method",
[black, borda, fptp, runoff, irv, coombs],
)
@given(election=complete_ranked_ballots(min_cands=2, max_cands=15,
min_voters=1, max_voters=60))
def test_ranked_methods_no_tiebreak_returns_none_or_id(method, election):
election = np.asarray(election)
winner = method(election)
n_cands = election.shape[1]
assert winner in {None} | set(range(n_cands))


if __name__ == "__main__":
# Run unit tests, in separate process to avoid warnings about cached
# modules, printing output line by line in realtime
Expand Down
14 changes: 13 additions & 1 deletion tests/test_sntv.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from hypothesis import given
from hypothesis.strategies import integers, lists, permutations

from elsim.methods import sntv
from elsim.methods import fptp, sntv


def collect_random_results(method, election):
Expand Down Expand Up @@ -291,6 +291,18 @@ def test_defaults(election):
assert sntv(election) == sntv(election, 1) == sntv(election, 1, None)


@given(election=complete_ranked_ballots(min_cands=2, max_cands=20,
min_voters=1, max_voters=80))
def test_sntv_one_seat_matches_fptp_order(election):
election = np.asarray(election)
f_w = fptp(election, tiebreaker='order')
s_w = sntv(election, 1, tiebreaker='order')
if f_w is None:
assert s_w is None
else:
assert s_w == {f_w}


if __name__ == "__main__":
# Run unit tests, in separate process to avoid warnings about cached
# modules, printing output line by line in realtime
Expand Down
Loading