Skip to content

Commit

Permalink
Add consistency to new voting choices in regards to prevous choices
Browse files Browse the repository at this point in the history
  • Loading branch information
janos committed Jan 20, 2024
1 parent 06fef8f commit 10274eb
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 73 deletions.
66 changes: 36 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,59 @@ White paper [Markus Schulze, "The Schulze Method of Voting"](https://arxiv.org/p

The act of voting represents calling the `Vote` function with a `Ballot` map where keys in the map are choices and values are their rankings. Lowest number represents the highest rank. Not all choices have to be ranked and multiple choices can have the same rank. Ranks do not have to be in consecutive order.

### Additional features

This implementation of Schulze voting method adds capabilities to

- remove the ballot from voting results, allowing the vote to be changed
- add, remove or rearrange choices at any time during the voting process, while preserving consistency of the state just as the choices were present from the beginning.

`Unvote` function allows to update the pairwise preferences in a way to cancel the previously added `Ballot` to preferences using `Vote` function. It is useful to change the vote without the need to re-vote all ballots.

`SetChoices` allows to update the pairwise preferences if the choices has to be changed during voting. New choices can be added, existing choices can be removed or rearranged. New choices will have no preferences against existing choices, neither existing choices will have preferences against the new choices.
`SetChoices` allows to update the pairwise preferences if the choices has to be changed during voting. New choices can be added, existing choices can be removed or rearranged. New choices are ranked as previous ballots did not rank them or were ranked the last, as they were present in initial choices but were not ranked in any ballots.

## Voting

`Voting` holds number of votes for every pair of choices. It is a convenient construct to use when the preferences slice does not have to be exposed, and should be kept safe from accidental mutation. Methods on the Voting type are not safe for concurrent calls.


## Example

```go
package main

import (
"fmt"
"log"
"fmt"
"log"

"resenje.org/schulze"
"resenje.org/schulze"
)

func main() {
choices := []string{"A", "B", "C"}
preferences := schulze.NewPreferences(len(choices))

// First vote.
if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{
"A": 1,
}); err != nil {
log.Fatal(err)
}

// Second vote.
if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{
"A": 1,
"B": 1,
"C": 2,
}); err != nil {
log.Fatal(err)
}

// Calculate the result.
result, tie := schulze.Compute(preferences, choices)
if tie {
log.Fatal("tie")
}
fmt.Println("winner:", result[0].Choice)
choices := []string{"A", "B", "C"}
preferences := schulze.NewPreferences(len(choices))

// First vote.
if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{
"A": 1,
}); err != nil {
log.Fatal(err)
}

// Second vote.
if err := schulze.Vote(preferences, choices, schulze.Ballot[string]{
"A": 1,
"B": 1,
"C": 2,
}); err != nil {
log.Fatal(err)
}

// Calculate the result.
result, tie := schulze.Compute(preferences, choices)
if tie {
log.Fatal("tie")
}
fmt.Println("winner:", result[0].Choice)
}
```

Expand Down
81 changes: 74 additions & 7 deletions schulze.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Record[C comparable] [][]C
// values. A record of a complete and normalized preferences is returned that
// can be used to unvote.
func Vote[C comparable](preferences []int, choices []C, b Ballot[C]) (Record[C], error) {
ranks, choicesCount, err := ballotRanks(choices, b)
ranks, choicesCount, hasUnrankedChoices, err := ballotRanks(choices, b)
if err != nil {
return nil, fmt.Errorf("ballot ranks: %w", err)
}
Expand All @@ -50,6 +50,27 @@ func Vote[C comparable](preferences []int, choices []C, b Ballot[C]) (Record[C],
}
}

// set diagonal values as the values of the column of the least ranked
// choice to be able to have the correct preferences matrix when adding new
// choices
if hasUnrankedChoices {
// treat the diagonal values as one of the unranked choices,
// deprioritizing all choices except unranked as they are of the same
if l := len(ranks); l > 0 {
for _, choices1 := range ranks[:l-1] {
for _, i := range choices1 {
preferences[int(i)*choicesCount+int(i)] += 1
}
}
}
} else {
// all choices are ranked, tread diagonal values as a single not ranked
// choice, deprioritizing them for all existing choices
for i := 0; i < choicesCount; i++ {
preferences[int(i)*choicesCount+int(i)] += 1
}
}

r := make([][]C, len(ranks))
for rank, indexes := range ranks {
if r[rank] == nil {
Expand All @@ -67,6 +88,11 @@ func Vote[C comparable](preferences []int, choices []C, b Ballot[C]) (Record[C],
func Unvote[C comparable](preferences []int, choices []C, r Record[C]) error {
choicesCount := len(choices)

recordLength := len(r)
if recordLength == 0 {
return nil
}

for rank, choices1 := range r {
rest := r[rank+1:]
for _, choice1 := range choices1 {
Expand All @@ -86,6 +112,40 @@ func Unvote[C comparable](preferences []int, choices []C, r Record[C]) error {
}
}

knownChoices := newBitset(uint64(choicesCount))
rankedChoices := newBitset(uint64(choicesCount))
// remove voting from the ranked choices of the Record
for _, choices1 := range r[:recordLength-1] {
for _, choice1 := range choices1 {
i := getChoiceIndex(choices, choice1)
if i < 0 {
continue
}
preferences[int(i)*choicesCount+int(i)] -= 1
knownChoices.set(uint64(i))
rankedChoices.set(uint64(i))
}
}
// mark the rest of the known choices in the Record
for _, choice1 := range r[recordLength-1] {
i := getChoiceIndex(choices, choice1)
if i < 0 {
continue
}
knownChoices.set(uint64(i))
}

// remove votes of the choices that were added after the Record
for i := uint64(0); int(i) < choicesCount; i++ {
if rankedChoices.isSet(i) {
for j := uint64(0); int(j) < choicesCount; j++ {
if !knownChoices.isSet(j) {
preferences[int(i)*choicesCount+int(j)] -= 1
}
}
}
}

return nil
}

Expand All @@ -104,8 +164,15 @@ func SetChoices[C comparable](preferences []int, current, updated []C) []int {
updatedPreferences[iUpdated*updatedLength+j] = preferences[iUpdated*currentLength+j]
} else {
jCurrent := int(getChoiceIndex(current, updated[j]))
if iCurrent >= 0 && jCurrent >= 0 {
updatedPreferences[iUpdated*updatedLength+j] = preferences[iCurrent*currentLength+jCurrent]
if iCurrent >= 0 {
if jCurrent >= 0 {
updatedPreferences[iUpdated*updatedLength+j] = preferences[iCurrent*currentLength+jCurrent]
} else {
// set the column of the new choice to the values of the
// preferences' diagonal values, just as nobody voted for the
// new choice to ensure consistency
updatedPreferences[iUpdated*updatedLength+j] = preferences[iCurrent*currentLength+iCurrent]
}
}
}
}
Expand Down Expand Up @@ -142,10 +209,10 @@ func getChoiceIndex[C comparable](choices []C, choice C) choiceIndex {
return -1
}

func ballotRanks[C comparable](choices []C, b Ballot[C]) (ranks [][]choiceIndex, choicesLen int, err error) {
func ballotRanks[C comparable](choices []C, b Ballot[C]) (ranks [][]choiceIndex, choicesLen int, hasUnrankedChoices bool, err error) {
choicesLen = len(choices)
ballotLen := len(b)
hasUnrankedChoices := ballotLen != choicesLen
hasUnrankedChoices = ballotLen != choicesLen

ballotRanks := make(map[int][]choiceIndex, ballotLen)
var rankedChoices bitSet
Expand All @@ -158,7 +225,7 @@ func ballotRanks[C comparable](choices []C, b Ballot[C]) (ranks [][]choiceIndex,
for choice, rank := range b {
index := getChoiceIndex(choices, choice)
if index < 0 {
return nil, 0, &UnknownChoiceError[C]{Choice: choice}
return nil, 0, false, &UnknownChoiceError[C]{Choice: choice}
}
ballotRanks[rank] = append(ballotRanks[rank], index)

Expand Down Expand Up @@ -197,7 +264,7 @@ func ballotRanks[C comparable](choices []C, b Ballot[C]) (ranks [][]choiceIndex,
}
}

return ranks, choicesLen, nil
return ranks, choicesLen, hasUnrankedChoices, nil
}

const intSize = unsafe.Sizeof(int(0))
Expand Down
Loading

0 comments on commit 10274eb

Please sign in to comment.