chiark / gitweb /
graphviz: new output, with -g option
[appendix-a6.git] / compute
1 #!/usr/bin/perl -w
2 #
3 # parses some input about ballots and computes the result according
4 # to Debian Constitution A.6.
5 #
6 # usage: .../compute [<input files>...]
7 #  (uses stdin if none supplied)
8 #
9 # input files can be in any order and should contain lines like:
10 #
11 #   <votername> :: <PREF>, (<PREF>=<PREF>), <PREF>, ....
12 #   V: <tallyprefs> <votername>
13 #   <ABBREV> = <description...>
14 #   <ABBREV> = <description...> [<rat>:<io>]
15 #   <ABBREV> = <description...> [default]
16 #   nodefault
17 #   quorum = <quorum>
18 #
19 # where
20 #
21 #   <votername> is any non-whitespace characters
22 #
23 #   <ABBREV> and <PREF> are abbreviations for choices on the ballot.
24 #      They must be ASCII alphanumeric.  Numeric preferences are
25 #      not recommended as they are confusing (particularly when
26 #      tally sheets are in use).
27 #
28 #      It is best to avoid FD, SQ and NOTA except for the default
29 #      option; if no default option is specified, it will default
30 #      to one of these unless a "nodefault" line is found.
31 #
32 #      In the "::" form, the commas are optional.  Either ( ) or = may
33 #      be used to indicate a set of equal preferences.
34 #
35 #   <tallyprefs> is a tally sheet preference order: that is,
36 #      a single uppercase base36 digit for each ballot choice,
37 #      (in the order they were specified by "=" lines), giving
38 #      the voter's preference rank for that option.  The numbers
39 #      need not be distinct or contiguous.  "-" may be used for
40 #      unranked preferences.
41 #
42 #   <rat>:<io> is the majority ratio for the option
43 #
44 #   <quorum> is the quorum
45 #
46 # and also allowed are
47 #
48 #   #-comments                            } all
49 #   blank lines                           } ignored
50 #   lines starting with whitespace        }
51
52 use strict;
53 use utf8;
54 use autodie;
55
56 use Data::Printer;
57 use Graph::Directed;
58 use Graph::Writer::GraphViz;
59
60 binmode STDIN, 'encoding(UTF-8)';
61 binmode STDOUT, 'encoding(UTF-8)';
62 binmode STDERR, 'encoding(UTF-8)';
63
64 our @choices;
65 our %choices;
66 our @invotes_v;
67 our @invotes_cc;
68 our $defcho;
69 our $quorum = 0;
70
71 our $gfile;
72
73 while (@ARGV && $ARGV[0] =~ /^-/) {
74     local $_ = shift @ARGV;
75     if (m/^--?$/) {
76         last;
77     } elsif (s/^-g//) {
78         $gfile = $_;
79     } else {
80         die "bad usage\n";
81     }
82 }
83
84 sub addchoice {
85     my $choname = shift @_;
86     my $cho = $choices{$choname} = {
87         @_, Index => (scalar @choices)
88     };
89     push @choices, $choname;
90     return $cho;
91 }
92
93 while (<>) {
94     s/\s+$//;
95     next if m/^\s*\#/;
96     next unless m/\S/;
97     next if m/^\s/;
98     if (m/^([A-Z0-9]+)\s*\=\s*(\S.*)$/) {
99         my ($choname, $desc) = ($1,$2);
100         my $cho = addchoice($choname, Desc => $desc);
101         if ($desc =~ m/\[(\d+):(\d+)\]/) {
102             $cho->{Smaj} = [$1,$2];
103         } elsif ($desc =~ m/\[default\]/) {
104             $defcho = $cho;
105         }
106     } elsif (m/^V:\s+(\S+)\s+(\S.*)/) {
107         push @invotes_v, [ $1, $2 ];
108     } elsif (m/^(\S+)\s*\:\:\s*/) {
109         push @invotes_cc, [ $1, "$'" ];
110     } elsif (m/^quorum = ([0-9.]+)$/) {
111         $quorum = $1+0.0;
112     } elsif (m/^nodefault$/) {
113         $defcho = { Index => -1 }
114     } else {
115         die "invalid input";
116     }
117 }
118
119 our @vab;
120 # $vab[$ia][$ib] = V(A,B)
121 # Actually, it's a list of voters who prefer A to B (A.6(3)(1))
122
123 # Go through the voters and construct V(A,B)
124
125 print "\nParsing \`simple' style ballots\n# devotee-tally-begin\n"
126         if @invotes_cc;
127 # actually, we pre-parse them into @invotes_v
128 # since we want to show them as a tally sheet anyway
129
130 foreach my $iv (@invotes_cc) {
131     $_ = uc $iv->[1];
132     foreach my $chn (m/\b\w+\b/g) {
133         next if $choices{$chn};
134         addchoice($chn, Desc => "($chn from voter ballot)");
135     }
136 }
137
138 foreach my $iv (@invotes_cc) {
139     my ($voter) = $iv->[0];
140     $_ = uc $iv->[1];
141
142     s/\t/ /g;
143     s/\,/ /g;
144     while (s{\(([^()]+)\)}{
145         my $x = $1; $x =~ s/[ =]+/=/g; $x;
146     }ge) { }
147     s/[ =]*=[ =*]/=/g;
148     s/\s+/ /g;
149     print "# normalised $_ ($voter)\n";
150
151     my @ranks = (1000,) x @choices;
152     my $rank = 1;
153     foreach (split /\s+/) {
154         foreach (split /=/) {
155             my $cho = $choices{$_};
156             $cho or die "unknown choice $_ ($voter)";
157             my $ix = $cho->{Index};
158             $ranks[$ix] = $rank;
159         }
160         $rank++;
161     }
162     my $vstr = join '', map {
163         $_ == 1000 ? "-" :
164         $_ < 10 ? (sprintf "%d", $_) :
165         $_ < 36 ? (chr(ord('A') + $_ - 10)) : die
166     } @ranks;
167     print "V: $vstr $voter\n";
168     push @invotes_v, [ $vstr, $voter ];
169 }
170
171 print "# devotee-tally-end\n"
172         if @invotes_cc;
173
174 print "\nDetermining default option\n";
175
176 if ($defcho && $defcho->{Index} > -1) {
177     print "default option was specified: $choices[$defcho->{Index}]\n";
178 } elsif ($defcho) {
179     print "no default option\n";
180 } else {
181     foreach my $try (qw(FD SQ NOTA)) {
182         $defcho = $choices{$try};
183         last if $defcho;
184     }
185     if ($defcho) {
186         print "guessed default option: $choices[$defcho->{Index}]\n";
187     } else {
188         print "could not guess default option, assuming there is none\n";
189     }
190 }
191
192 my $defi = $defcho->{Index};
193 die "FD has smaj?!" if $defcho->{Smaj};
194
195 print "\nParsing devotee tally sheet ballots\n" 
196     if @invotes_v > @invotes_cc;
197
198 foreach my $iv (@invotes_v) {
199     my ($votestr,$voter) = @$iv;
200     eval {
201         length $votestr eq @choices or die "wrong vote vector length";
202         my @vs = split //, $votestr;
203         foreach my $ix (0..$#vs) {
204             my $vchr = $vs[$ix];
205             if ($vchr eq '-') {
206                 $vs[$ix] = 1000;
207             } elsif ($vchr =~ m/[0-9a-z]/) {
208                 $vs[$ix] = ord($vchr);
209             } else {
210                 die "bad vote char";
211             }
212         }
213         foreach my $ia (0..$#vs) {
214             foreach my $ib ($ia+1..$#vs) {
215                 my $va = $vs[$ia];
216                 my $vb = $vs[$ib];
217                 if ($va < $vb) { push @{ $vab[$ia][$ib] }, $voter }
218                 elsif ($vb < $va) { push @{ $vab[$ib][$ia] }, $voter }
219             }
220         }
221     };
222     die "voter $voter $@" if $@;
223 }
224
225 print "\nPreference matrix\n";
226
227 our @ch = map { $choices{$_} } @choices;
228
229 # Print the counts V(A,B)
230 foreach my $iy (-2..$#ch) {
231     foreach my $ix (-2..$#ch) {
232         if ($iy==-1) {
233             if ($ix==-1) {
234                 printf "+";
235             } else {
236                 printf "------";
237             }
238         } elsif ($ix==-1) {
239             printf "|";
240         } elsif ($ix==-2 && $iy==-2) {
241             printf "V(Y,X)";
242         } elsif ($iy==-2) {
243             printf "%5s ", $choices[$ix];
244         } elsif ($ix==-2) {
245             printf "%5s ", $choices[$iy];
246         } else {
247             my $v = \( $vab[$iy][$ix] );
248             $$v ||= [ ];
249             if (@$$v) {
250                 printf "%5d ", (scalar @$$v);
251             } else {
252                 printf "%5s ", "";
253             }
254         }
255     }
256     printf "\n";
257 }
258
259 sub drop ($$) {
260     my ($i,$why) = @_;
261     print "dropping $choices[$i]: $why\n";
262     $ch[$i]{Dropped} = $why;
263 }
264
265 if (defined $defi) {
266     print "\nQuorum A.6(2) (quorum is $quorum)\n";
267
268     foreach my $i (0..$#choices) {
269         next if $ch[$i]{Dropped};
270         next if $i == $defi;
271         my $v = $vab[$i][$defi];
272         next if $v >= $quorum;
273         drop $i, "quorum ($v < $quorum)";
274     }
275
276     print "\nMajority ratio A.6(3)\n";
277
278     foreach my $i (0..$#choices) {
279         next if $ch[$i]{Dropped};
280         next if $i == $defi;
281         my $majr = $ch[$i]{Smaj};
282         $majr ||= [1,1]; # A.6(3)(3)
283         my $vad = scalar @{ $vab[$i][$defi] };
284         my $vda = scalar @{ $vab[$defi][$i] };
285         next if $vad * $majr->[1] > $vda * $majr->[0];
286         drop $i, "majority ratio ($vad:$vda <= $majr->[1]:$majr->[0])";
287     }
288 }
289
290 print "\nDefeats A.6(4)\n";
291
292 my $defeats = Graph::Directed->new;
293
294 sub chremain () {
295     return grep { !$ch[$_]{Dropped} } (0..$#ch);
296 }
297
298 foreach my $ia (chremain()) {
299     $defeats->add_vertex($choices[$ia]);
300     foreach my $ib (chremain()) {
301         my $vab = scalar @{ $vab[$ia][$ib] };
302         my $vba = scalar @{ $vab[$ib][$ia] };
303         next unless $vab > $vba;
304         my $diff = $vab - $vba;
305         print "defeat: $choices[$ia] beats $choices[$ib]",
306             " ($vab > $vba = +$diff)\n";
307         $defeats->add_edge($choices[$ia],$choices[$ib]);
308     }
309 }
310
311 sub chvab ($$) {
312     my ($ca,$cb) = @_;
313     my $v = $vab[ $choices{$ca}{Index} ][ $choices{$cb}{Index} ];
314     return scalar @$v;
315 }
316
317 sub weaker ($$) {
318     # A.6(7)(1)
319     my ($def1,$def2) = @_;
320     my ($ca,$cx) = @$def1;
321     my ($cb,$cy) = @$def2;
322     return 1 if chvab($ca, $cx) < chvab($cb, $cy);
323     return 1 if chvab($ca, $cx) == chvab($cb, $cy)
324               && chvab($cx, $ca) > chvab($cy, $cb);
325     return 0;
326 }
327
328 our $showg = $defeats->deep_copy();
329
330 our $schwartz;
331
332 for (;;) {
333     # loop from A6(5)
334
335     print "defeats graph: $defeats\n";
336
337     print "\nTransitive closure A.6(5)\n";
338
339     my $tdefeats = $defeats->transitive_closure();
340
341     # this makes the debugging output prettier
342     foreach my $ch (@choices) {
343         $tdefeats->delete_edge($ch,$ch);
344     }
345     print "closure graph: $tdefeats\n";
346
347     print "\nSchwartz set A.6(6)\n";
348     
349     $schwartz = $defeats->copy();
350
351     foreach my $ia (chremain()) {
352         foreach my $ib (chremain()) {
353             next if $tdefeats->has_edge($choices[$ia],$choices[$ib]);
354             next if !$tdefeats->has_edge($choices[$ib],$choices[$ia]);
355             print "not in Schwartz set: $choices[$ia] because $choices[$ib]\n";
356             $schwartz->delete_vertex($choices[$ia]);
357             last;
358         }
359     }
360
361     print "set: ", (join " ", $schwartz->vertices()), "\n";
362
363     print "\nDropping weakest defeats A.6(7)\n";
364
365     our @weakest = ();
366
367     foreach my $edge ($schwartz->edges()) {
368 #       print " considering @$edge\n";
369         if (!@weakest) {
370             # no weakest edges yet
371         } elsif (weaker($edge, $weakest[0])) {
372             # this edge is weaker than previous weakest, start new set
373             @weakest = ();
374         } elsif (weaker($weakest[0], $edge)) {
375             # weakest edge is weaker than this one, ignore this one
376             next;
377         } else {
378             # weakest edge is exactly as weak as this one, add this one
379         }
380         push @weakest, $edge;
381     }
382
383     last unless @weakest;
384
385     my $w = $weakest[0];
386     printf "weakest defeats are %d > %d\n", 
387         chvab($w->[0], $w->[1]),
388         chvab($w->[1], $w->[0]);
389     foreach my $weakest (@weakest) {
390         my ($ca,$cb) = @$weakest;
391         print "a weakest defeat is $ca > $cb\n";
392         $defeats->delete_edge($ca,$cb);
393     }
394
395     print "\nDefeats within the Schwartz set, go round again\n";
396 }
397
398 print "no defeats within the Schwartz set\n";
399 print "final schwartz set:\n\n";
400
401 if ($schwartz->vertices() == 1) {
402     print "WINNER IS:\n";
403 } else {
404     print "WINNER IS ONE OF (CASTING VOTE DECIDES):\n";
405 }
406
407 printf "    %-5s %s\n", $_, $choices{$_}{Desc}
408     foreach ($schwartz->vertices());
409
410 if (defined $gfile) {
411     foreach my $cho (values %choices) {
412         my $chn = $choices[$cho->{Index}];
413         my $label = "\\N\n$cho->{Desc}";
414         if ($cho->{Dropped}) {
415             $label .= "\nDropped: $cho->{Dropped}";
416         }
417         $showg->set_vertex_attribute($chn, 'label', $label);
418     }
419
420     my $gwriter = new Graph::Writer::GraphViz -format => 'ps';
421     $gwriter->write_graph($showg, $gfile);
422 }
423
424 print ".\n";
425
426 #p %choices;
427 #p @vab;