From c9b64d56f3606fff45225db6567110a840b35e72 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Sun, 3 Nov 2024 23:28:07 +0100 Subject: [PATCH] Add scripts to help with determining results The random generate script is useful to test OpaVote/CIVS in advance Please forgive me for using bash ':) --- nix/generate.sh | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ nix/process.sh | 62 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100755 nix/generate.sh create mode 100755 nix/process.sh diff --git a/nix/generate.sh b/nix/generate.sh new file mode 100755 index 00000000..7d32ebcb --- /dev/null +++ b/nix/generate.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Generates a randomised OpaVote ballot file (https://opavote.com/help/overview#blt-file-format) to stdout + +export BC_ENV_ARGS="-lq" + +# Every voter has a normal-distributed preference X + +# 25 voters and 10 candidates is the maximum for the free tier +voterCount=450 +# 9 so that we have candidates -2, -1.5, ..., 0, ..., 1.5, 2, instead of messy numbers +candidateCount=23 +seatCount=7 +# With 40%, a minority group should get a representative +minorityPercentage=15 + + +# Voters are represented with a single number. They vote for the candidate nearest to their number +voterValues=() +pi=$(echo "scale=10; 4*a(1)" | bc -l) +normal() { + bc <<< "$1 * sqrt(-2 * l($(( RANDOM )) / 32767 )) * c(2 * $pi * $(( RANDOM )) / 32767) + $2" +} + +for i in $(seq "$voterCount"); do + voterValues+=( "$(normal 2 0)" ) + # Check proportional representation works + #if (( 100 * i / voterCount <= minorityPercentage )); then + # # The minority group votes with a wide standard deviation around candidate (-3) + # # such that no single candidate gets more 1st ranks than the majority group. + # # We want to test that this group still get a representative seat regardless + # voterValues+=( "$(normal 2 -3)" ) + #else + # # The majority group all votes effectively for the same candidate (3) with only minor differences, + # # which ensures that a number of candidates that could in theory fill all seats get most 1st and 2nd ranks, + # # but we don't want that to happen because of proportional representation + # voterValues+=( "$(normal 0.5 3)" ) + #fi +done + +# Sort the voters by their value number and therefore also ballots +readarray -t voterValues < <(printf "%s\n" "${voterValues[@]}" | sort -n) +declare -p voterValues >&2 + +# Candidates are uniformly distributed from -4 to +4, somewhat matching what would happen in reality, +# where people want to have a representative to vote for, and if they don't, they'd nominate themselves. +candidates=() +for (( i=1; i <= candidateCount; i++ )); do + candidates[i]=$(bc <<< "scale=1; ($i - 1) * 8 / ($candidateCount - 1) - 4") +done +declare -p candidates >&2 + +# Start generating the file + +echo "$candidateCount $seatCount" + +for value in "${voterValues[@]}"; do + #echo "New voter: $value" + readarray -t rankedCandidates < <( + for candidate in "${!candidates[@]}"; do + # Calculate the distance between the voters value and the candidates + echo "$candidate $(bc <<< "($value - ${candidates[$candidate]}) ^ 2")" + done | sort -n -k2 | cut -d' ' -f1) + + #declare -p rankedCandidates + weight=1 + end=0 + echo "$weight" "${rankedCandidates[@]}" "$end" +done +echo 0 + +for candidate in "${candidates[@]}"; do + echo "\"$candidate\"" +done + +echo "\"Test Election\"" diff --git a/nix/process.sh b/nix/process.sh new file mode 100755 index 00000000..7a67922d --- /dev/null +++ b/nix/process.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# Converts an OpaVote BLT file (https://opavote.com/help/overview#blt-file-format) from stdin +# to a CIVS ballot file (https://github.com/andrewcmyers/civs/blob/867d2c68b0259cac6e0269515b56edb1c49a8003/cgi-bin/languages/base_language.pm#L326-L344) to stdout + +# Summary of the CIVS format: +# Each line contains the ranks of the N choices. Ranks are numbers from 1 to N +# 2 4 3 1 5 A simple ballot ranking five choices. The top-ranked choice is the fourth one. + +# However, entirely undocumented right now, for the best-candidate (the opposite of combined weights/ratings) voter criteria, which we're using, the rankings are inverted! +# https://github.com/andrewcmyers/civs/blob/867d2c68b0259cac6e0269515b56edb1c49a8003/cgi-bin/vote#L176-L178 +# https://github.com/andrewcmyers/civs/blob/867d2c68b0259cac6e0269515b56edb1c49a8003/cgi-bin/results#L468-L475 + +set -euo pipefail + +read -r candidateCount _seatCount; +echo "Candidate count: $candidateCount" >&2 +while read -ra ballot; do + if [[ "${#ballot[@]}" == 1 && "${ballot[0]}" == "0" ]]; then + echo "Read all ballots" >&2 + break + else + weight=${ballot[0]} + unset 'ballot[0]' + end=${ballot[-1]} + unset 'ballot[-1]' + if [[ "$weight" != 1 || "$end" != 0 ]]; then + echo "Problematic ballot: First number (weight) is $weight (should be 1), last number (end marker) is $end (should be 0)" >&2 + exit 1 + fi + + declare -a candidateRanks=() + for candidate in $(seq "${candidateCount}"); do + candidateRanks[candidate]=$candidateCount + done + + # Inverts the ranking, see top of file! + rank=$candidateCount + for candidate in "${ballot[@]}"; do + candidateRanks[candidate]=$(( rank-- )) + done + + echo "${ballot[@]}" "->" "${candidateRanks[@]}" >&2 + echo "${candidateRanks[@]}" + fi +done + +echo "Candidates (fill this into the candidate field):" >&2 +for candidate in $(seq "${candidateCount}"); do + read -r name + name=${name%\"} + name=${name#\"} + if [[ "$name" == "0" ]]; then + # For some reason CIVS doesn't store candidates named "0" + echo "0.0" >&2 + else + echo "$name" >&2 + fi +done + +read -r title +echo "Title: $title" >&2