File Coverage

blib/lib/Vote/Count/Method/CondorcetVsIRV.pm
Criterion Covered Total %
statement 176 181 97.2
branch 45 50 90.0
condition 4 9 44.4
subroutine 25 25 100.0
pod 2 8 25.0
total 252 273 92.3


line stmt bran cond sub pod time code
1 2     2   1227 use strict;
  2         4  
  2         102  
2 2     2   12 use warnings;
  2         5  
  2         55  
3 2     2   32 use 5.024;
  2         8  
4 2     2   11 use feature qw /postderef signatures/;
  2         5  
  2         205  
5              
6             package Vote::Count::Method::CondorcetVsIRV;
7 2     2   17 use namespace::autoclean;
  2         4  
  2         17  
8 2     2   253 use Moose;
  2         6  
  2         18  
9              
10             with 'Vote::Count::Log';
11              
12 2     2   15433 use Storable 3.15 'dclone';
  2         37  
  2         131  
13 2     2   15 use Vote::Count::ReadBallots qw/read_ballots write_ballots/;
  2         5  
  2         107  
14 2     2   1054 use Vote::Count::Redact qw/RedactSingle RedactPair RedactBullet/;
  2         6  
  2         128  
15 2     2   962 use Vote::Count::Method::CondorcetIRV;
  2         6  
  2         75  
16 2     2   14 use Try::Tiny;
  2         4  
  2         115  
17 2     2   12 use Data::Dumper;
  2         4  
  2         165  
18              
19             our $VERSION='2.01';
20              
21             # no warnings 'uninitialized';
22 2     2   14 no warnings qw/experimental/;
  2         5  
  2         4352  
23              
24             =head1 NAME
25              
26             Vote::Count::Method::CondorcetVsIRV
27              
28             =head1 VERSION 2.01
29              
30             =cut
31              
32             # ABSTRACT: Condorcet versus IRV
33              
34             =pod
35              
36             =head1 SYNOPSIS
37              
38             use Vote::Count::Method::CondorcetVsIRV;
39              
40             my $Election = Vote::Count::Method::CondorcetVsIRV->new( ... );
41             my $result = $Election->CondorcetVsIRV();
42             or
43             my $Election = Vote::Count->new( TieBreakMethod => 'approval' );
44             my $result = $Election->CondorcetVsIRV( relaxed => 1 );
45             equivalent to default:
46             my $result = $Election->CondorcetVsIRV( relaxed => 0, smithsetirv => 0 );
47              
48             say $result->{'winner'};
49              
50             $Election->WriteAllLogs();
51              
52             Returns a HashRef with a key for winner.
53              
54             =head1 Method Common Name: Condorcet vs IRV
55              
56             Condorcet vs IRV Methods determine if the Condorcet Winner needed votes from the IRV winner; electing the Condorcet Winner if there was not a later harm violation, electing the IRV winner if there was. If there is no Condorcet Winner the IRV Winner is chosen.
57              
58             To determine if there was a violation the ballots of one or more choices are redacted, with later choice on those ballots removed.
59              
60             With these methods it is also possible to allow a tolerance for Later Harm.
61              
62             =head3 Double Redaction
63              
64             The Double Redaction method (default) measures the later harm effect between a Condorcet Winner and the IRV Winner.
65              
66             Considering the Margin of the Condorcet Winner over the IRV Winner and the number of votes needed by the Condorcet Winner from the IRV winner as measures of Preference for the Condorcet Winner and of Later Harm, it is also possible to establish a Later Harm Tolerance Threshold.
67              
68             The Relaxed Later Harm option will select the Condorcet Winner when their margin of victory over the IRV Winner is greater than the number of later votes they need from the IRV Winner to be a Condorcet Winner. Although not presently implemented a different ratio or percentage could be used.
69              
70             Because in most cases where the IRV and Condorcet winners are different there are Later Harm effects, without relaxed this method will almost always confirm the IRV winner.
71              
72             =head3 Simple (Single Redaction)
73              
74             This variation only redacts the ballots that choose the IRV Winner as their first choice. This gives the voters confidence that if their first choice wins by the later harm safe method, that their vote will not be used against that choice.
75              
76             The simplest form is:
77              
78             1. Determine the IRV Winner
79              
80             2. Treating the ballots cast with the IRV Winner as their first choice as ballots cast only for the IRV Winner, determine the Condorcet Winner.
81              
82             3. Elect the Condorcet Winner, if there is none, elect the IRV Winner.
83              
84             Unfortunately, this simplest form, in cases where more than one choice defeats the IRV Winner in pairing and later choices of the IRV Winner's ballots determine which becomes the Condorcet Winner, removes the supporters of the IRV Winner from the final decision.
85              
86             The form implemented by Vote::Count is:
87              
88             =over
89              
90             1. Determine both the IRV and Condorcet Winner. If they are the same, elect that choice. If there is no Condorcet Winner, elect the IRV Winner.
91              
92             2. Treating the ballots cast with the IRV Winner as their first choice as ballots cast for only the IRV Winner determine the Condorcet Winner.
93              
94             3. If there is a Condorcet Winner, elect the first Condorcet Winner, if there is none, elect the IRV Winner. (The redaction cannot make the IRV Winner a Condorcet Winner if it isn't already one).
95              
96             =back
97              
98             =cut
99              
100             =head1 Criteria
101              
102             The double redaction version is later harm safe if the relaxed option is not used. The simple version later harm protects first choice votes only, it also does not protect the first Condorcet Winner's votes at all.
103              
104             =head2 Simplicity
105              
106             The simple version does not require Condorcet Loop resolution, and thus can be considered to be on par with Benham for complexity, and like Benham is Hand Countable. The double redaction version is more complex, but is perhaps more valuable as an approach for measuring later harm.
107              
108             =head2 Later Harm
109              
110             This method meets Later Harm with the default strict option.
111              
112             The relaxed option allows a finite Later Harm effect.
113              
114             Using the TCA Floor Rule and or Smith Set IRV add small Later Harm effects.
115              
116             =head2 Condorcet Criteria
117              
118             This method only meets Condorcet Loser, when the IRV winner is chosen instead of the Condorcet Winner, the winner may be outside the Smith Set.
119              
120             =head2 Consistency
121              
122             Because this method chooses between the outcomes of two different methods, it is subject to the consistency failings of both. Given that Cloning is an important consistency issue in real elections, the clone handling should be an improvement over IRV.
123              
124             =head1 Implementation
125              
126             Details specific to this implementation.
127              
128             The Tie Breaker is defaulted to (modified) Grand Junction for resolvability. Any Tie Breaker supported by Vote::Count::TieBreaker may be used, 'all' and 'none' are not recommended.
129              
130             =head2 Function Name: CondorcetVsIRV
131              
132             Runs the election, returns a hashref containing the winner, similar to how other Vote::Count Methods such as RunIRV behave.
133              
134             =head3 Arguments for CondorcetVsIRV()
135              
136             =over
137              
138             =item* relaxed
139              
140             =item* simple
141              
142             =item* smithsetirv
143              
144             =back
145              
146             =head2 LogTo, LogPath, LogBaseName, LogRedactedTo
147              
148             The first three behave as normal Vote::Count::Log methods, except that the default is /tmp/condorcetvsirv.
149              
150             LogRedactedTo defaults to appending _redacted into the log names for the redacted election, it can be overridden by setting a value (which should be /path/basename) like LogTo.
151              
152             =head2 WriteLog WriteAllLogs
153              
154             WriteLog behaves normally, there is a log set for the CondorcetVSIRV object as well as child logs for the Election and RedactedElection, each of which has a set of logs for PairMatrix as well. WriteAllLogs will write all of these logs.
155              
156             =cut
157              
158             # LogTo over-writes role LogTo changing default filename.
159             has 'LogTo' => (
160             is => 'rw',
161             isa => 'Str',
162             default => '/tmp/condorcetvsirv',
163             );
164              
165             has 'LogRedactedTo' => (
166             is => 'lazy',
167             is => 'rw',
168             isa => 'Str',
169             builder => '_setredactedlog',
170             );
171              
172 22     22   78570 sub _setredactedlog ( $self ) {
  22         63  
  22         57  
173             # There is a bug with LogTo being uninitialized despite having a default
174 22 100       782 my $logto
175             = defined $self->LogTo()
176             ? $self->LogTo() . '_redacted'
177             : '/tmp/condorcetvsirv_redacted';
178 22         82 return $logto;
179             }
180              
181             has 'TieBreakMethod' => (
182             is => 'ro',
183             isa => 'Str',
184             default => 'grandjunction',
185             );
186              
187             has 'BallotSet' => ( is => 'ro', isa => 'HashRef', required => 1 );
188              
189             has 'Active' => (
190             is => 'rw',
191             isa => 'HashRef',
192             lazy => 1,
193             builder => '_InitialActive',
194             );
195              
196 22     22   58 sub _InitialActive ( $I ) { return dclone $I->BallotSet()->{'choices'} }
  22         45  
  22         38  
  22         702  
197              
198 4     4 0 11 sub SetActive ( $I, $active ) {
  4         8  
  4         12  
  4         8  
199 4         169 $I->{'Active'} = dclone $active;
200 4         42 $I->{'Election'}->SetActive($active);
201 4 100       17 if ( defined $I->{'RedactedElection'} ) {
202 2         8 $I->{'RedactedElection'}->SetActive($active);
203             }
204             }
205              
206 1     1 0 3 sub ResetActive ( $self ) {
  1         2  
  1         3  
207 1         41 my $new = dclone $self->BallotSet()->{'choices'};
208 1         6 $self->SetActive($new);
209 1         3 return $new;
210             }
211              
212             # sub ResetActive ( $self ) { return dclone $self->BallotSet()->{'choices'} }
213              
214 19     19   43 sub _CVI_IRV ( $I, $active, $smithsetirv ) {
  19         53  
  19         79  
  19         48  
  19         46  
215 19         41 my $WonIRV = undef;
216 19         43 my $irvresult = undef;
217 19 100       77 if ($smithsetirv) {
218 2         85 $irvresult = $I->SmithSetIRV( $I->TieBreakMethod() );
219             }
220             else {
221 17         601 $irvresult = $I->RunIRV( $active, $I->TieBreakMethod() );
222             }
223 19         180 $I->logd( 'IRV Result: ' . Dumper $irvresult );
224 19 100       164 return $irvresult->{'winner'} if $irvresult->{'winner'};
225 1         7 $I->logt("IRV ended with a Tie.");
226             $I->logt(
227 1         7 "Active (Tied) Choices are: " . join( ', ', $irvresult->{'tied'} ) );
228 1         15 $I->SetActiveFromArrayRef( $irvresult->{'tied'} );
229 1         6 return 0;
230             }
231              
232             sub BUILD {
233 22     22 0 37330 my $self = shift;
234             $self->{'Election'} = Vote::Count::Method::CondorcetIRV->new(
235             BallotSet => $self->BallotSet(),
236             TieBreakMethod => $self->TieBreakMethod(),
237             Active => $self->Active(),
238 22         862 LogTo => $self->{'LogTo'} . '_unredacted',
239             );
240 22         4627 $self->{'RedactedElection'} = undef,;
241             }
242              
243 1     1 1 3 sub WriteAllLogs ($I) {
  1         3  
  1         2  
244 1         6 $I->WriteLog();
245 1         498 $I->Election()->WriteLog();
246 1         510 $I->RedactedElection()->WriteLog();
247 1         9101 $I->Election()->PairMatrix()->WriteLog();
248 1         460 $I->RedactedElection()->PairMatrix()->WriteLog();
249             }
250              
251 35     35 0 109 sub Election ($self) { return $self->{'Election'} }
  35         68  
  35         86  
  35         217  
252              
253 15     15 0 32 sub RedactedElection ( $self, $ballotset = undef, $active = undef ) {
  15         31  
  15         36  
  15         30  
  15         26  
254 15         182 return $self->{'RedactedElection'};
255             }
256              
257 11     11 0 39 sub CreateRedactedElection ( $self, $WonCondorcet, $WonIRV, $simpleflag=0 ) {
  11         27  
  11         33  
  11         33  
  11         27  
  11         23  
258 11 100       429 my $ballotset = $simpleflag
259             ? RedactBullet ( $self->BallotSet(), $WonIRV )
260             : RedactPair( $self->BallotSet(), $WonCondorcet, $WonIRV );
261 11         573 $self->{'RedactedElection'} = Vote::Count->new(
262             BallotSet => $ballotset,
263             TieBreakMethod => $self->TieBreakMethod(),
264             Active => $self->Active(),
265             LogTo => $self->LogRedactedTo(),
266             );
267             $self->logd(
268             'Created Redacted Election.',
269             $self->{'RedactedElection'}->PairMatrix()->PairingVotesTable(),
270 11         319 $self->{'RedactedElection'}->PairMatrix()->MatrixTable(),
271             );
272             }
273              
274 10     10   31 sub _CVI_RedactRun ( $I, $WonCondorcet, $WonIRV, $active, $options ) {
  10         29  
  10         24  
  10         28  
  10         27  
  10         26  
  10         23  
275             my $smithsetirv
276 10 50       50 = $options->{'smithsetirv'} ? 1 : 0;
277 10 100       41 my $relaxed = $options->{'relaxed'} ? $options->{'relaxed'} : 0;
278 10 100       39 my $simpleflag = $options->{'simple'} ? 1 : 0;
279 10         54 my $E = $I->Election();
280 10         55 my $R = $I->RedactedElection();
281 10         314 my $ConfirmC = $R->PairMatrix->CondorcetWinner();
282 10         62 my $ConfirmI = _CVI_IRV( $R, $active, $smithsetirv );
283 10 100       49 if ( $ConfirmC ) {
284 9 100 66     67 if ( $simpleflag ) {
    100          
285 3         26 $I->logt("Elected $WonCondorcet, Redacted Ballots had a Condorcet Winner.");
286 3 100       20 $I->logv("The Redacted Condorcet Winner was $ConfirmC.")
287             if ( $ConfirmC ne $WonCondorcet);
288 3         17 return $WonCondorcet ;
289             } elsif ( $ConfirmC eq $WonCondorcet or $ConfirmC eq $WonIRV ) {
290 3         23 $I->logt("Elected $ConfirmC, Redacted Ballots Condorcet Winner.");
291 3         16 return $ConfirmC;
292             }
293             } else {
294 1         4 $ConfirmI = _CVI_IRV( $R, $active, $smithsetirv );
295 1 50 33     26 if ( $ConfirmI eq $WonCondorcet or $ConfirmI eq $WonIRV ) {
296 0         0 $I->logt("Elected $ConfirmI, Redacted Ballots IRV Winner.");
297 0         0 return $ConfirmI;
298             }
299             }
300 4 100       84 $ConfirmC = 'NONE' unless $ConfirmC;
301 4         36 $I->logt("Neither $WonCondorcet nor $WonIRV were confirmed.");
302 4         27 $I->logt(
303             "Redacted Ballots Winners: Condorcet = $ConfirmC, IRV = $ConfirmI");
304 4 100       44 if ($relaxed) {
305 2         55 my $GreatestLoss = $R->PairMatrix()->GreatestLoss($WonCondorcet);
306             my $Margin = $E->PairMatrix()->GetPairResult( $WonCondorcet, $WonIRV )
307 2         56 ->{'margin'};
308 2         12 $I->logt(
309             "The margin of the Condorcet over the IRV winner was: $Margin");
310 2         14 $I->logt(
311             "$WonCondorcet\'s greatest loss with redacted ballots was $GreatestLoss."
312             );
313 2 100       10 if ( $Margin > $GreatestLoss ) {
314 1         6 $I->logt("Elected: $WonCondorcet");
315 1         5 return $WonCondorcet;
316             }
317             }
318 3         18 $I->logt("Elected: $WonIRV");
319 3         14 return $WonIRV;
320             }
321              
322 21     21 1 3651 sub CondorcetVsIRV ( $self, %args ) {
  21         49  
  21         83  
  21         47  
323 21         93 my $E = $self->Election();
324 21 100       108 my $smithsetirv = defined $args{'smithsetirv'} ? $args{'smithsetirv'} : 0;
325 21 100       96 my $simpleflag = defined $args{'simple'} ? $args{'simple'} : 0;
326 21         910 my $active = $self->Active();
327             # check for majority winner.
328 21         154 my $majority = $E->EvaluateTopCountMajority()->{'winner'};
329 21 50       81 return $majority if $majority;
330 21         47 my $WonIRV = undef;
331 21         716 my $WonCondorcet = $E->PairMatrix()->CondorcetWinner();
332 21 100       83 if ($WonCondorcet) {
333 13         112 $self->logt("Condorcet Winner is $WonCondorcet");
334             # Even if SmithSetIRV requested, it would return the condorcet winner
335             # We need to know if a different choice would win IRV.
336 13         407 $WonIRV = $E->RunIRV( $active, $E->TieBreakMethod() )->{'winner'};
337             }
338             else {
339 8         67 $self->logt("No Condorcet Winner");
340 8         54 $WonIRV = _CVI_IRV( $E, $active, $smithsetirv );
341 8 100       39 if ($WonIRV) {
342 7         51 $self->logt("Electing IRV Winner $WonIRV");
343 7         101 return { 'winner' => $WonIRV };
344             }
345             else { ;
346 1         6 $self->logt("There is no Condorcet or IRV winner.");
347 1         14 return { 'winner' => 0 };
348             }
349             }
350              
351             # IRV private already logged tie, now return the false value.
352             # Edge case IRV tie with Condorcet Winner, I guess CW wins?
353 13 100       63 unless ($WonIRV) {
354 1 50       5 if ($WonCondorcet) {
355 1         8 $self->logt("Electing Condorcet Winner $WonCondorcet, IRV tied.");
356 1         10 return { 'winner' => $WonCondorcet};
357             }
358 0         0 return { 'winner' => 0 };
359             }
360 12 100       51 if ( $WonIRV eq $WonCondorcet ) {
361 2         20 $self->logt("Electing $WonIRV the winner by both Condorcet and IRV.");
362 2         19 return { 'winner' => $WonIRV };
363             }
364 10 50 33     77 if ( $WonIRV and !$WonCondorcet ) {
365 0         0 $self->logt(
366             "Electing IRV Winner $WonIRV. There was no Condorcet Winner.");
367 0         0 return { 'winner' => $WonIRV };
368             }
369 10         72 $self->CreateRedactedElection( $WonCondorcet, $WonIRV, $simpleflag );
370 10         315 my $winner
371             = $self->_CVI_RedactRun( $WonCondorcet, $WonIRV, $active, \%args );
372 10         92 return { 'winner' => $winner };
373              
374             }
375              
376             1;
377              
378             #FOOTER
379              
380             =pod
381              
382             BUG TRACKER
383              
384             L<https://github.com/brainbuz/Vote-Count/issues>
385              
386             AUTHOR
387              
388             John Karr (BRAINBUZ) brainbuz@cpan.org
389              
390             CONTRIBUTORS
391              
392             Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.
393              
394             LICENSE
395              
396             This module is released under the GNU Public License Version 3. See license file for details. For more information on this license visit L<http://fsf.org>.
397              
398             SUPPORT
399              
400             This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.
401              
402             =cut
403