Skip to content

Commit

Permalink
update README
Browse files Browse the repository at this point in the history
  • Loading branch information
milselarch committed May 5, 2024
1 parent 43b164f commit 714db21
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 27 deletions.
77 changes: 65 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Ranked Choice Voting (RCV) implementation using Tries in Rust.

RCV differs from normal first past the post voting in that voters are allowed
to rank candidates from most to least preferable. To determine the winner of an RCV election, the
least votes for the least popular candidate(s) are transferred to their next choice until
some candidate reaches a majority.
votes for the least popular candidate each round is transferred to the next candidate in the
respective ranked votes until some candidate achieves an effective majority.

Example usage:
```rust
Expand All @@ -15,8 +15,8 @@ use trie_rcv::vote::RankedVote;

fn main() {
let mut rcv = RankedChoiceVoteTrie::new();
// remove all candidates with the lowest number of votes each round
rcv.set_elimination_strategy(EliminationStrategies::EliminateAll);

rcv.insert_vote(RankedVote::from_vector(&vec![1, 2, 3, 4]).unwrap());
rcv.insert_vote(RankedVote::from_vector(&vec![1, 2, 3]).unwrap());
rcv.insert_vote(RankedVote::from_vector(&vec![3]).unwrap());
Expand All @@ -41,35 +41,88 @@ fn main() {
}
```

This implementation also supports votes containing `withhold` and `abstain` votes,
where the `withhold` vote allows the voter to declare for none of the candidates, and
the abstain vote allows the voter to voluntarily remove himself from the poll
(this is useful for improving the chances that the rest of the votes are able
to conclude with a winning candidate)
This implementation also supports ranked votes ending
with `SpecialVotes::WITHHOLD` and `SpecialVotes::ABSTAIN` values:
1. `SpecialVotes::WITHHOLD`
Allows the voter to explicitly declare for none of the candidates.
Qualitatively this allows a voter to declare a vote of no confidence.
Serializes to `-1` via `SpecialVotes::WITHHOLD.to_int()`
2. `SpecialVotes::ABSTAIN`
Allows the voter to explicitly declare for none of the candidates while also
voluntarily removing himself from the poll.
Qualitatively this allows a voter to indicate
that he wants one of the candidates to win but isn't able to decide for himself and
would thus want to delegate the decision to the rest of the electorate.
Serializes to `-2` via `SpecialVotes::ABSTAIN.to_int()`

```rust
use trie_rcv;
use trie_rcv::RankedChoiceVoteTrie;
use trie_rcv::vote::{RankedVote, SpecialVotes};

fn main() {
let votes = RankedVote::from_vectors(&vec![
let rcv = RankedChoiceVoteTrie::new();

let votes_round1 = RankedVote::from_vectors(&vec![
vec![1, SpecialVotes::WITHHOLD.to_int()],
vec![2, 1],
vec![3, 2],
vec![3]
]).unwrap();

let rcv = RankedChoiceVoteTrie::new();
let winner = rcv.run_election(votes);
let winner_round1 = rcv.run_election(votes_round1);
println!("WINNER = {:?}", winner);
assert_eq!(
winner, None, concat![
winner_round1, None, concat![
"Candidate 1's vote should not count after round 1, ",
"no one should have majority"
]);

let votes_round2 = RankedVote::from_vectors(&vec![
vec![1, SpecialVotes::ABSTAIN.to_int()],
vec![2, 1],
vec![3, 2],
vec![3]
]).unwrap();

let winner_round2 = rcv.run_election(votes_round2);
println!("WINNER = {:?}", winner_round2);
assert_eq!(
winner_round2, Some(3), concat![
"First vote is ignored in round 2, candidate 3 wins"
]);
}
```

### Elimination Strategies
Technically the RCV algorithm specification doesn't state what to do in the situation that
there are multiple candidates who all have the same, lowest number of votes in some round during
RCV. The `elimination_strategy` setting handles how to deal with this edge case:

1. `EliminationStrategies::ElimnateAll`
Removes all candidates with the lowest number of votes each round.
2. `EliminationStrategies::DowdallScoring` (default)
Among multiple candidates with the lowest number of votes each round,
sort the candidates by their dowdall score and eliminate the candidate(s)
with the lowest [Dowdall score](https://rdrr.io/cran/votesys/man/dowdall_method.html).
The Dowdall score for each candidate is calculated by
the sum of the inverse of the ranking (starting from 1) for each ranked vote.
If a ranked vote does not contain a candidate, then it does not count
towards the dowdall score)
3. `EliminationStrategies::RankedPairs`
Among multiple candidates with the lowest number of votes each round, attempt
to construct a directed acyclic graph establishing a pecking order between
these candidates via [ranked-pair](https://en.wikipedia.org/wiki/Ranked_pairs)
comparisons, whereby candidate A is said to be better than candidate B
if there are more votes that rank A higher than B
than vice versa.
1. Note that if a ranked vote ranks A explicitly but not B, then that is
counted as ranking A higher than B as well
2. In the event that a directed acyclic preference graph cannot be established
(such as when there are cyclic preferences between candidates), then the elimination
behavior will default to eliminating all candidates with the same,
lowest number of votes each round i.e. it will fall back to the
behavior of `EliminationStrategies::ElimnateAll`

## Build instructions
Build crate using `cargo build`, run integration tests with `cargo test`
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ struct VoteTransferChanges<'a> {
// strategies for how to eliminate candidates each round
#[derive(Clone, PartialEq)]
pub enum EliminationStrategies {
// eliminate all candidates with the lowest number of votes
// removes all candidates with the lowest number of votes each round
EliminateAll,
// eliminate the candidate(s) with both the lowest number of votes
// followed by the lowest dowdall score
Expand Down
30 changes: 16 additions & 14 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn test_tie_scenario() {
}

#[test]
fn test_zero_vote_end() {
fn test_withold_vote_end() {
let votes = RankedVote::from_vectors(&vec![
vec![1, WITHOLD_VOTE_VAL],
vec![2, 1],
Expand All @@ -90,34 +90,36 @@ fn test_zero_vote_end() {
]);
}

#[test]
fn test_zero_nil_votes_only() {
fn test_abstain_vote_end() {
let votes = RankedVote::from_vectors(&vec![
vec![WITHOLD_VOTE_VAL],
vec![WITHOLD_VOTE_VAL],
vec![WITHOLD_VOTE_VAL],
vec![ABSTAIN_VOTE_VAL]
vec![1, ABSTAIN_VOTE_VAL],
vec![2, 1],
vec![3, 2],
vec![3]
]).unwrap();

let rcv = RankedChoiceVoteTrie::new();
let winner = rcv.run_election(votes);
println!("WINNER = {:?}", winner);
assert_eq!(winner, None);
assert_eq!(
winner, Some(3), concat![
"First vote is ignored in round 2, candidate 3 wins"
]);
}

#[test]
fn test_null_vote_end() {
fn test_withhold_votes_only() {
let votes = RankedVote::from_vectors(&vec![
vec![1, ABSTAIN_VOTE_VAL],
vec![2, 1],
vec![3, 2],
vec![3]
vec![WITHOLD_VOTE_VAL],
vec![WITHOLD_VOTE_VAL],
vec![WITHOLD_VOTE_VAL],
vec![ABSTAIN_VOTE_VAL]
]).unwrap();

let rcv = RankedChoiceVoteTrie::new();
let winner = rcv.run_election(votes);
println!("WINNER = {:?}", winner);
assert_eq!(winner, Some(3));
assert_eq!(winner, None);
}

#[test]
Expand Down

0 comments on commit 714db21

Please sign in to comment.