stv_rs/
pbv.rs

1// Copyright 2023 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A simulation of plurality block voting based on ranked ballots.
16
17use crate::arithmetic::{Integer, IntegerRef, Rational, RationalRef};
18use crate::types::{Election, ElectionResult};
19use log::{debug, info, trace};
20use std::io;
21
22/// A simulation of plurality block voting, where each ballot attributes 1 vote
23/// to the first N candidates (where N is the number of seats), and 0 votes to
24/// the following ones.
25///
26/// More precisely:
27/// - If the ballot strictly ranks at least N candidates, the first N candidates
28///   in the ballot receive 1 vote each.
29/// - If the ballot ranks less than N candidates, all the ranked candidates
30///   receive 1 vote each.
31/// - If the ballot is structured as X candidates ranked, then K candidates
32///   ranked equally, then possibly more candidates ranked, with `X < N < X +
33///   K`, then the first X candidates receive 1 vote each, and the next K
34///   candidates receive `(N - X) / K` votes each (i.e. the remaining `N - X`
35///   votes are split equally among the K candidates ranked equally).
36pub fn plurality_block_voting<I, R>(
37    stdout: &mut impl io::Write,
38    election: &Election,
39    package_name: &str,
40    votes_per_ballot: Option<usize>,
41) -> io::Result<ElectionResult>
42where
43    I: Integer + Send + Sync,
44    for<'a> &'a I: IntegerRef<I>,
45    R: Rational<I> + Send + Sync,
46    for<'a> &'a R: RationalRef<&'a I, R>,
47{
48    let votes_per_ballot = votes_per_ballot.unwrap_or(election.num_seats);
49    info!(
50        "Plurality block voting with {} seats / {} candidates, votes per ballot = {votes_per_ballot}",
51        election.num_seats, election.num_candidates
52    );
53
54    writeln!(
55        stdout,
56        r"
57Election: {}
58
59	{package_name}
60	Rule: Simulated Plurality Block Voting
61	Arithmetic: {}
62	Seats: {}
63	Ballots: {}
64	Votes per ballot: {votes_per_ballot}
65",
66        election.title,
67        R::description(),
68        election.num_seats,
69        election.num_ballots,
70    )?;
71
72    let mut sum = vec![R::zero(); election.num_candidates];
73    for (i, ballot) in election.ballots.iter().enumerate() {
74        trace!("Processing ballot {i} = {:?}", ballot);
75
76        let mut votes_distributed = 0;
77        for ranking in ballot.order() {
78            let rank_len = ranking.len();
79            let weight = if votes_distributed + rank_len <= votes_per_ballot {
80                // Ballot still has enough power for all candidates at this rank.
81                R::one()
82            } else {
83                // Split the remaining power equally among candidates at this rank.
84                R::from_usize(votes_per_ballot - votes_distributed) / I::from_usize(rank_len)
85            };
86            trace!(
87                "  - {weight} * {:?}",
88                ranking.iter().map(|&x| x.into()).collect::<Vec<_>>()
89            );
90
91            let ballot_count = R::from_usize(ballot.count());
92            for &candidate in ranking {
93                sum[candidate.into()] += &weight * &ballot_count;
94            }
95
96            votes_distributed += rank_len;
97            if votes_distributed >= votes_per_ballot {
98                break;
99            }
100        }
101    }
102
103    let mut order: Vec<usize> = (0..election.num_candidates).collect();
104    order.sort_by(|&i, &j| {
105        sum[j].partial_cmp(&sum[i]).unwrap().then_with(|| {
106            election
107                .tie_order
108                .get(&i)
109                .unwrap()
110                .cmp(election.tie_order.get(&j).unwrap())
111        })
112    });
113
114    debug!("Sums:");
115    for (i, &c) in order.iter().enumerate() {
116        debug!("    [{i}] {} = {}", election.candidates[c].nickname, sum[c]);
117    }
118
119    let elected: Vec<usize> = order.iter().cloned().take(election.num_seats).collect();
120    let not_elected: Vec<usize> = order
121        .iter()
122        .cloned()
123        .rev()
124        .take(election.num_candidates - election.num_seats)
125        .filter(|&i| !election.candidates[i].is_withdrawn)
126        .collect();
127
128    writeln!(stdout, "Action: Count Complete")?;
129    info!("Elected:");
130    for (i, &id) in elected.iter().enumerate() {
131        info!("    [{}] {}", i, election.candidates[id].nickname);
132        writeln!(stdout, "Action: Elect: {}", election.candidates[id].name)?;
133    }
134
135    info!("Not elected:");
136    for (i, &id) in not_elected.iter().enumerate() {
137        info!(
138            "    [{}] {}",
139            election.num_candidates - i - 1,
140            election.candidates[id].nickname
141        );
142        writeln!(stdout, "Action: Defeat: {}", election.candidates[id].name)?;
143    }
144
145    writeln!(stdout, "Action: Count Complete")?;
146    for &id in elected.iter() {
147        writeln!(
148            stdout,
149            "\tElected:  {} ({})",
150            election.candidates[id].name, sum[id]
151        )?;
152    }
153    for &id in not_elected.iter().rev() {
154        writeln!(
155            stdout,
156            "\tDefeated: {} ({})",
157            election.candidates[id].name, sum[id]
158        )?;
159    }
160
161    let result = ElectionResult {
162        elected,
163        not_elected,
164    };
165    Ok(result)
166}
167
168#[cfg(test)]
169mod test {
170    use super::*;
171    use crate::arithmetic::{FixedDecimal9, Integer64};
172    use crate::types::{Ballot, Candidate};
173
174    fn make_election() -> Election {
175        Election::builder()
176            .title("Vegetable contest")
177            .num_seats(5)
178            .candidates([
179                Candidate::new("apple", false),
180                Candidate::new("banana", true),
181                Candidate::new("cherry", false),
182                Candidate::new("date", false),
183                Candidate::new("eggplant", false),
184                Candidate::new("fig", true),
185                Candidate::new("grape", false),
186                Candidate::new("hazelnut", false),
187                Candidate::new("jalapeno", false),
188            ])
189            .ballots([
190                Ballot::new(1, [vec![0], vec![2, 3, 4], vec![6]]),
191                Ballot::new(2, [vec![2], vec![3, 4], vec![6, 7]]),
192                Ballot::new(3, [vec![2, 3], vec![4, 6], vec![7, 8]]),
193                Ballot::new(4, [vec![3, 4], vec![6, 7], vec![8, 0]]),
194                Ballot::new(5, [vec![4], vec![6, 7, 8], vec![0]]),
195                Ballot::new(6, [vec![6], vec![7, 8, 0], vec![2]]),
196                Ballot::new(7, [vec![6, 7], vec![8, 0], vec![2, 3]]),
197                Ballot::new(8, [vec![7, 8], vec![0, 2], vec![3, 4]]),
198                Ballot::new(9, [vec![8, 0], vec![2, 3], vec![4]]),
199            ])
200            .build()
201    }
202
203    #[test]
204    fn test_plurality_block_voting() {
205        let election = make_election();
206
207        let mut buf = Vec::new();
208        let result = plurality_block_voting::<Integer64, FixedDecimal9>(
209            &mut buf,
210            &election,
211            "package name",
212            None,
213        )
214        .unwrap();
215        assert_eq!(
216            result,
217            ElectionResult {
218                elected: vec![8, 0, 7, 2, 4],
219                not_elected: vec![3, 6]
220            }
221        );
222        assert_eq!(
223            std::str::from_utf8(&buf).unwrap(),
224            r"
225Election: Vegetable contest
226
227	package name
228	Rule: Simulated Plurality Block Voting
229	Arithmetic: fixed-point decimal arithmetic (9 places)
230	Seats: 5
231	Ballots: 45
232	Votes per ballot: 5
233
234Action: Count Complete
235Action: Elect: Jalapeno
236Action: Elect: Apple
237Action: Elect: Hazelnut
238Action: Elect: Cherry
239Action: Elect: Eggplant
240Action: Defeat: Date
241Action: Defeat: Grape
242Action: Count Complete
243	Elected:  Jalapeno (38.500000000)
244	Elected:  Apple (38.000000000)
245	Elected:  Hazelnut (33.500000000)
246	Elected:  Cherry (32.500000000)
247	Elected:  Eggplant (28.000000000)
248	Defeated: Grape (28.000000000)
249	Defeated: Date (26.500000000)
250"
251        );
252    }
253
254    #[test]
255    fn test_plurality_block_voting_1() {
256        let election = make_election();
257
258        let mut buf = Vec::new();
259        let result = plurality_block_voting::<Integer64, FixedDecimal9>(
260            &mut buf,
261            &election,
262            "package name",
263            Some(1),
264        )
265        .unwrap();
266        assert_eq!(
267            result,
268            ElectionResult {
269                elected: vec![6, 8, 7, 4, 0],
270                not_elected: vec![3, 2]
271            }
272        );
273        assert_eq!(
274            std::str::from_utf8(&buf).unwrap(),
275            r"
276Election: Vegetable contest
277
278	package name
279	Rule: Simulated Plurality Block Voting
280	Arithmetic: fixed-point decimal arithmetic (9 places)
281	Seats: 5
282	Ballots: 45
283	Votes per ballot: 1
284
285Action: Count Complete
286Action: Elect: Grape
287Action: Elect: Jalapeno
288Action: Elect: Hazelnut
289Action: Elect: Eggplant
290Action: Elect: Apple
291Action: Defeat: Date
292Action: Defeat: Cherry
293Action: Count Complete
294	Elected:  Grape (9.500000000)
295	Elected:  Jalapeno (8.500000000)
296	Elected:  Hazelnut (7.500000000)
297	Elected:  Eggplant (7.000000000)
298	Elected:  Apple (5.500000000)
299	Defeated: Cherry (3.500000000)
300	Defeated: Date (3.500000000)
301"
302        );
303    }
304
305    #[test]
306    fn test_plurality_block_voting_2() {
307        let election = make_election();
308
309        let mut buf = Vec::new();
310        let result = plurality_block_voting::<Integer64, FixedDecimal9>(
311            &mut buf,
312            &election,
313            "package name",
314            Some(2),
315        )
316        .unwrap();
317        assert_eq!(
318            result,
319            ElectionResult {
320                elected: vec![8, 7, 6, 0, 4],
321                not_elected: vec![2, 3]
322            }
323        );
324        assert_eq!(
325            std::str::from_utf8(&buf).unwrap(),
326            r"
327Election: Vegetable contest
328
329	package name
330	Rule: Simulated Plurality Block Voting
331	Arithmetic: fixed-point decimal arithmetic (9 places)
332	Seats: 5
333	Ballots: 45
334	Votes per ballot: 2
335
336Action: Count Complete
337Action: Elect: Jalapeno
338Action: Elect: Hazelnut
339Action: Elect: Grape
340Action: Elect: Apple
341Action: Elect: Eggplant
342Action: Defeat: Cherry
343Action: Defeat: Date
344Action: Count Complete
345	Elected:  Jalapeno (20.666666663)
346	Elected:  Hazelnut (18.666666663)
347	Elected:  Grape (14.666666665)
348	Elected:  Apple (11.999999998)
349	Elected:  Eggplant (10.333333333)
350	Defeated: Date (8.333333333)
351	Defeated: Cherry (5.333333333)
352"
353        );
354    }
355
356    #[test]
357    fn test_plurality_block_voting_3() {
358        let election = make_election();
359
360        let mut buf = Vec::new();
361        let result = plurality_block_voting::<Integer64, FixedDecimal9>(
362            &mut buf,
363            &election,
364            "package name",
365            Some(3),
366        )
367        .unwrap();
368        assert_eq!(
369            result,
370            ElectionResult {
371                elected: vec![8, 7, 0, 6, 2],
372                not_elected: vec![4, 3]
373            }
374        );
375        assert_eq!(
376            std::str::from_utf8(&buf).unwrap(),
377            r"
378Election: Vegetable contest
379
380	package name
381	Rule: Simulated Plurality Block Voting
382	Arithmetic: fixed-point decimal arithmetic (9 places)
383	Seats: 5
384	Ballots: 45
385	Votes per ballot: 3
386
387Action: Count Complete
388Action: Elect: Jalapeno
389Action: Elect: Hazelnut
390Action: Elect: Apple
391Action: Elect: Grape
392Action: Elect: Cherry
393Action: Defeat: Eggplant
394Action: Defeat: Date
395Action: Count Complete
396	Elected:  Jalapeno (27.833333326)
397	Elected:  Hazelnut (24.333333326)
398	Elected:  Apple (21.499999996)
399	Elected:  Grape (19.833333330)
400	Elected:  Cherry (14.166666666)
401	Defeated: Date (14.166666666)
402	Defeated: Eggplant (13.166666666)
403"
404        );
405    }
406
407    #[test]
408    fn test_plurality_block_voting_4() {
409        let election = make_election();
410
411        let mut buf = Vec::new();
412        let result = plurality_block_voting::<Integer64, FixedDecimal9>(
413            &mut buf,
414            &election,
415            "package name",
416            Some(4),
417        )
418        .unwrap();
419        assert_eq!(
420            result,
421            ElectionResult {
422                elected: vec![8, 0, 7, 6, 2],
423                not_elected: vec![4, 3]
424            }
425        );
426        assert_eq!(
427            std::str::from_utf8(&buf).unwrap(),
428            r"
429Election: Vegetable contest
430
431	package name
432	Rule: Simulated Plurality Block Voting
433	Arithmetic: fixed-point decimal arithmetic (9 places)
434	Seats: 5
435	Ballots: 45
436	Votes per ballot: 4
437
438Action: Count Complete
439Action: Elect: Jalapeno
440Action: Elect: Apple
441Action: Elect: Hazelnut
442Action: Elect: Grape
443Action: Elect: Cherry
444Action: Defeat: Eggplant
445Action: Defeat: Date
446Action: Count Complete
447	Elected:  Jalapeno (35.000000000)
448	Elected:  Apple (31.000000000)
449	Elected:  Hazelnut (31.000000000)
450	Elected:  Grape (26.000000000)
451	Elected:  Cherry (23.000000000)
452	Defeated: Date (19.000000000)
453	Defeated: Eggplant (15.000000000)
454"
455        );
456    }
457
458    #[test]
459    fn test_plurality_block_voting_6() {
460        let election = make_election();
461
462        let mut buf = Vec::new();
463        let result = plurality_block_voting::<Integer64, FixedDecimal9>(
464            &mut buf,
465            &election,
466            "package name",
467            Some(6),
468        )
469        .unwrap();
470        assert_eq!(
471            result,
472            ElectionResult {
473                elected: vec![8, 0, 2, 7, 3],
474                not_elected: vec![6, 4]
475            }
476        );
477        assert_eq!(
478            std::str::from_utf8(&buf).unwrap(),
479            r"
480Election: Vegetable contest
481
482	package name
483	Rule: Simulated Plurality Block Voting
484	Arithmetic: fixed-point decimal arithmetic (9 places)
485	Seats: 5
486	Ballots: 45
487	Votes per ballot: 6
488
489Action: Count Complete
490Action: Elect: Jalapeno
491Action: Elect: Apple
492Action: Elect: Cherry
493Action: Elect: Hazelnut
494Action: Elect: Date
495Action: Defeat: Grape
496Action: Defeat: Eggplant
497Action: Count Complete
498	Elected:  Jalapeno (42.000000000)
499	Elected:  Apple (40.000000000)
500	Elected:  Cherry (36.000000000)
501	Elected:  Hazelnut (35.000000000)
502	Elected:  Date (34.000000000)
503	Defeated: Eggplant (32.000000000)
504	Defeated: Grape (28.000000000)
505"
506        );
507    }
508}