Implement WA STV and update documentation

This commit is contained in:
RunasSudo 2021-07-22 00:40:01 +10:00
parent 2ef7bf24f2
commit 3ea1eef7c5
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 88 additions and 21 deletions

View File

@ -4,18 +4,31 @@
The preset dropdown allows you to choose from a hardcoded list of preloaded STV counting rules. These are: The preset dropdown allows you to choose from a hardcoded list of preloaded STV counting rules. These are:
* *OpenTally WIGM*: A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic. <table>
* *Scottish STV*: Rules from the [*Scottish Local Government Elections Order 2011*](https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made), using the weighted inclusive Gregory method. Validated against the [2007 Scottish local government election result for Linn ward](https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn). <tr><th>Method</th><th>Description</th><th>Exceptions</th><th>Validated against</th></tr>
* [*Meek STV*](http://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf): Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system. <tr><td>OpenTally WIGM</td><td>A recommended set of simple STV rules designed for computer counting, using the weighted inclusive Gregory method and rational arithmetic.</td><td></td><td></td></tr>
* *Meek STV (1987)* operates according to the original Hill–Wichmann–Woodall [‘Algorithm 123’](https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf) specification of Meek STV, with the modifications, relevant only in exceptional cases, that (a) fixed-point arithmetic with 5 decimal places is used, and (b) candidates are elected on strictly exceeding the quota. Validated against the Hill–Wichmann–Woodall implementation for ballot papers [derived from the ERS97 model election](https://yingtongli.me/blog/2021/01/04/ers97.html). <tr><td>Scottish STV</td><td>Rules from the <a href="https://www.legislation.gov.uk/ssi/2011/399/schedule/1/made"><i>Scottish Local Government Elections Order 2011</i></a>, using the weighted inclusive Gregory method.</td><td></td><td><a href="https://web.archive.org/web/20121004213938/http://www.glasgow.gov.uk/en/YourCouncil/Elections_Voting/Election_Results/ElectionScotland2007/LGWardResults.htm?ward=1&wardname=1%20-%20Linn">2007 Linn ward local election</a> (eSTV 2.0.16)</td></tr>
* *Meek STV (2006)* operates according to [Hill's 2006 revisions](http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf). This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules. Validated against OpenSTV 1.7 for the ERS97 model election. <tr><td>Meek STV</td><td>Advanced STV rules designed for computer counting, recognised by the Proportional Representation Society of Australia (Victoria–Tasmania) as the superior STV system.</td><td></td><td></td></tr>
* *Meek STV (New Zealand)* operates according to Schedule 1A of the [*Local Electoral Regulations 2001*](https://www.legislation.govt.nz/regulation/public/2001/0145/latest/DLM57125.html). Validated against OpenSTV 1.7, and Hill's nzmeek version 6.7.7, for the ERS97 model election. <tr><td>&bullet; OpenTally Meek</td><td>Operates according to the original 1987 Hill–Wichmann–Woodall <a href="https://www.dia.govt.nz/diawebsite.NSF/Files/meekm/%24file/meekm.pdf">‘Algorithm 123’</a> specification of Meek STV, except that (a) ties are broken backwards then at random, (b) fixed-point arithmetic with 5 decimal places is used, and (c) candidates are elected on strictly exceeding the quota.</td><td></td><td><a href="https://yingtongli.me/blog/2021/01/04/ers97.html">Ballot papers derived from the ERS97 model election</a> (Hill–Wichmann–Woodall implementation)</td></tr>
* *Australian Senate STV*: Rules from the [*Commonwealth Electoral Act 1918*](https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700), using the unweighted inclusive Gregory method. Validated against the [2019 Australian Senate election result for Tasmania](https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm). <tr><td>&bullet; Meek STV (2006)</td><td>Operates according to <a href="http://www.votingmatters.org.uk/ISSUE22/I22P2.pdf">Hill's 2006 revisions</a>. This is the algorithm referred to in OpenSTV/OpaVote as ‘Meek STV’, and forms the basis of New Zealand's Meek STV rules.</td><td>[E1]</td><td><a href="https://yingtongli.me/blog/2021/01/04/ers97.html">Ballot papers derived from the ERS97 model election</a> (<a href="https://github.com/Conservatory/openstv">OpenSTV 1.7</a>)</td></tr>
* [*Wright STV*](https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf): Rules proposed by Anthony van der Craats designed for computer counting, involving reset and re-iteration of the count after each candidate exclusion. Validated against the [EVE Online reference implementation](https://github.com/ccpgames/ccp-wright-stv) for the [CSM 15 election](https://www.eveonline.com/news/view/meet-the-new-council). <tr><td>&bullet; Meek STV (New Zealand)</td><td>Operates according to Schedule 1A of the <a href="https://www.legislation.govt.nz/regulation/public/2001/0145/latest/DLM57125.html"><i>Local Electoral Regulations 2001</i></a>.</td><td>[E1]</td><td><a href="https://yingtongli.me/blog/2021/01/04/ers97.html">Ballot papers derived from the ERS97 model election</a> (OpenSTV 1.7, <a href="https://yingtongli.me/blog/2021/07/08/nzmeek.html">Hill's nzmeek 6.7.7</a>)</td></tr>
* [*PRSA 1977*](https://www.prsa.org.au/rule1977.htm): Simple rules designed for hand counting, using the exclusive Gregory method, with counting automatically performed in thousandths of a vote. Validated against [example 1](https://www.prsa.org.au/example1.pdf) of the PRSA's [*Proportional Representation Manual*](https://www.prsa.org.au/publicat.htm#p2). <tr><td>Australian Senate STV</td><td>Rules from the <a href="https://www.legislation.gov.au/Details/C2020C00400/Html/Text#_Toc59107700"><i>Commonwealth Electoral Act 1918</i></a>, using the unweighted inclusive Gregory method.</td><td>[E2] [E3]</td><td><a href="https://results.aec.gov.au/24310/Website/SenateDownloadsMenu-24310-Csv.htm">2019 Tasmanian Senate election</a> (AEC EasyCount)</td></tr>
* [*ERS97*](https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/): More complex rules designed for hand counting, using the exclusive Gregory method. Validated against the ERS97 model election. <tr><td>Western Australia STV</td><td>Rules from the <a href="https://www.legislation.wa.gov.au/legislation/prod/filestore.nsf/FileURL/mrdoc_29498.pdf/$FILE/Electoral Act 1907 - [17-a0-06].pdf"><i>Electoral Act 1907</i> (WA)</a>, using the weighted inclusive Gregory method.</td><td>[E2] [E3]</td><td></td></tr>
* *ERS76*: Former rules from the 1976 2nd edition. The quota is always calculated to 2 decimal places – for full ERS76 compliance, set *Round quota to 0 d.p.* when the quota is more than 100. <tr><td><a href="https://www.aph.gov.au/Parliamentary_Business/Committees/House_of_Representatives_Committees?url=em/elect07/subs/sub051.1.pdf">Wright STV</a></td><td>Rules proposed by Anthony van der Craats designed for computer counting, involving reset and re-iteration of the count after each candidate exclusion.</td><td></td><td><a href="https://www.eveonline.com/news/view/meet-the-new-council">EVE Online CSM 15 election</a> (<a href="https://github.com/ccpgames/ccp-wright-stv">EVE reference implementation</a>)</td></tr>
* *ERS73*: Former rules from the 1973 1st edition. The quota is always calculated to 2 decimal places – for full ERS73 compliance, set *Round quota to 0 d.p.* when the quota is 100 or more. <tr><td><a href="https://www.prsa.org.au/rule1977.htm">PRSA 1977</a></td><td>Simple rules designed for hand counting, using the exclusive Gregory method, with counting performed in thousandths of a vote.</td><td></td><td><a href="https://www.prsa.org.au/example1.pdf">Example 1</a> of the PRSA's <a href="https://www.prsa.org.au/publicat.htm#p2"><i>Proportional Representation Manual</i></a></td></tr>
<tr><td><a href="https://www.electoral-reform.org.uk/latest-news-and-research/publications/how-to-conduct-an-election-by-the-single-transferable-vote-3rd-edition/">ERS97</a></td><td>More complex rules designed for hand counting, using the exclusive Gregory method.</td><td></td><td><a href="https://yingtongli.me/blog/2021/01/04/ers97.html">Ballot papers derived from the ERS97 model election</a></td></tr>
<tr><td>&bullet; ERS76</td><td>Former rules from the 1976 2nd edition.</td><td>[E4]</td><td></td></tr>
<tr><td>&bullet; ERS73</td><td>Former rules from the 1973 1st edition.</td><td>[E4]</td><td></td></tr>
<tr><td>Church of England</td><td>Rules from the Church of England <a href="https://www.churchofengland.org/sites/default/files/2020-02/STV Rules 2020 - final.pdf"><i>Single Transferable Vote Rules 2020</i></a>, similar to ERS73.</td><td></td><td></td></tr>
</table>
[E1]: When generating random numbers, OpenTally uses a [deterministic random number generator based on SHA-256](rng.md), rather than the Wichmann–Hill(-based) algorithm.
[E2]: When breaking ties forwards/backwards, OpenTally selects the candidate who had more/fewer votes at the first/last stage when *any* tied candidate had more/fewer votes than the others, rather than when each all had unequal votes.
[E3]: A tie between 2 candidates for the final vacancy will be broken backwards then at random, rather than the method described in the legislation.
[E4]: The quota is always calculated to 2 decimal places. For full ERS76 (ERS73) compliance, set *Round quota to 0 d.p.* when the quota is more than 100 (100 or more).
This functionality is not available on the command line. This functionality is not available on the command line.
@ -202,12 +215,12 @@ When *Surplus method* is set to *Meek method*:
* --round-tvs instead controls the rounding of each intermediate product when computing candidates' votes * --round-tvs instead controls the rounding of each intermediate product when computing candidates' votes
* --round-votes controls the rounding of the final number of votes credited to each candidate * --round-votes controls the rounding of the final number of votes credited to each candidate
### Sum surplus transfers (--sum-surplus-transfers) ### (Gregory) Sum surplus transfers (--sum-surplus-transfers)
This option allows you to specify how the numbers of votes credited to candidates in a surplus transfer is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently: This option allows you to specify how the numbers of votes credited to candidates in a surplus transfer is calculated. In each case, votes are grouped according to the next available preference for a continuing candidate. Subsequently:
* *Single step*: The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product is credited to that candidate. * *Single step*: The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product is credited to that candidate.
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate. * *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product is credited to that candidate. This is distinct to *Single step* only for weighted inclusive Gregory.
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product is credited to that candidate. * *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product is credited to that candidate.
This option affects the result only insofar as rounding (due to use of fixed-precision arithmetic, or due to an explicit rounding option) is concerned. This option affects the result only insofar as rounding (due to use of fixed-precision arithmetic, or due to an explicit rounding option) is concerned.

View File

@ -38,17 +38,19 @@
<optgroup label="Recommended methods"> <optgroup label="Recommended methods">
<option value="wigm" selected>OpenTally WIGM</option> <option value="wigm" selected>OpenTally WIGM</option>
<option value="scottish">Scottish STV</option> <option value="scottish">Scottish STV</option>
<option value="meek87">Meek STV (1987)</option> <option value="meek87">OpenTally Meek</option>
</optgroup> </optgroup>
<optgroup label="Other methods"> <optgroup label="Other methods">
<option value="meek06">Meek STV (2006)</option> <option value="meek06">Meek STV (2006)</option>
<option value="meeknz">Meek STV (New Zealand)</option> <option value="meeknz">Meek STV (New Zealand)</option>
<option value="senate">Australian Senate STV</option> <option value="senate">Australian Senate STV</option>
<option value="wa">Western Australia STV</option>
<option value="wright">Wright STV</option> <option value="wright">Wright STV</option>
<option value="prsa77">PRSA 1977</option> <option value="prsa77">PRSA 1977</option>
<option value="ers97">ERS97</option> <option value="ers97">ERS97</option>
<option value="ers76">ERS76</option> <option value="ers76">ERS76</option>
<option value="ers73">ERS73</option> <option value="ers73">ERS73</option>
<option value="cofe">Church of England</option>
</optgroup> </optgroup>
</select> </select>
</label> </label>
@ -247,6 +249,7 @@
</label> </label>
</div> </div>
<label class="col-12"> <label class="col-12">
<span class="pill-grey" title="This option has effect only if “Method” is a Gregory method">Gregory</span>
Sum surplus transfers: Sum surplus transfers:
<select id="selSumTransfers"> <select id="selSumTransfers">
<option value="single_step" selected>Single step</option> <option value="single_step" selected>Single step</option>

View File

@ -435,7 +435,7 @@ function changePreset() {
document.getElementById('selTransfers').value = 'meek'; document.getElementById('selTransfers').value = 'meek';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'single_stage'; document.getElementById('selExclusion').value = 'single_stage';
document.getElementById('selTies').value = 'backwards,random'; document.getElementById('selTies').value = 'forwards,random';
} else if (document.getElementById('selPreset').value === 'meeknz') { } else if (document.getElementById('selPreset').value === 'meeknz') {
document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
@ -463,7 +463,7 @@ function changePreset() {
document.getElementById('selTransfers').value = 'meek'; document.getElementById('selTransfers').value = 'meek';
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'single_stage'; document.getElementById('selExclusion').value = 'single_stage';
document.getElementById('selTies').value = 'backwards,random'; document.getElementById('selTies').value = 'forwards,random';
} else if (document.getElementById('selPreset').value === 'senate') { } else if (document.getElementById('selPreset').value === 'senate') {
document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
@ -487,6 +487,29 @@ function changePreset() {
document.getElementById('selPapers').value = 'both'; document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'by_value'; document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'backwards,random'; document.getElementById('selTies').value = 'backwards,random';
} else if (document.getElementById('selPreset').value === 'wa') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = false;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '0';
document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '0';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '0';
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSumTransfers').value = 'by_value';
document.getElementById('selSurplus').value = 'by_order';
document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'by_source';
document.getElementById('selTies').value = 'backwards,random';
} else if (document.getElementById('selPreset').value === 'wright') { } else if (document.getElementById('selPreset').value === 'wright') {
document.getElementById('selQuotaCriterion').value = 'geq'; document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop'; document.getElementById('selQuota').value = 'droop';
@ -609,5 +632,30 @@ function changePreset() {
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'by_value'; document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'forwards,random'; document.getElementById('selTies').value = 'forwards,random';
} else if (document.getElementById('selPreset').value === 'cofe') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = false;
document.getElementById('chkDeferSurpluses').checked = true;
document.getElementById('selNumbers').value = 'fixed';
document.getElementById('txtDP').value = '5';
document.getElementById('txtPPDP').value = '2';
document.getElementById('chkNormaliseBallots').checked = false;
document.getElementById('chkRoundQuota').checked = true;
document.getElementById('txtRoundQuota').value = '2';
document.getElementById('chkRoundVotes').checked = true;
document.getElementById('txtRoundVotes').value = '2';
document.getElementById('chkRoundTVs').checked = true;
document.getElementById('txtRoundTVs').value = '2';
document.getElementById('chkRoundWeights').checked = true;
document.getElementById('txtRoundWeights').value = '2';
document.getElementById('selSumTransfers').value = 'per_ballot';
document.getElementById('selSurplus').value = 'by_size';
document.getElementById('selTransfers').value = 'eg';
document.getElementById('selPapers').value = 'transferable';
document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'forwards,random';
} }
} }

View File

@ -84,7 +84,7 @@ struct STV {
#[clap(help_heading=Some("ROUNDING"), long, value_name="dps")] #[clap(help_heading=Some("ROUNDING"), long, value_name="dps")]
round_quota: Option<usize>, round_quota: Option<usize>,
/// How to calculate votes to credit to candidates in surplus transfers /// (Gregory STV) How to calculate votes to credit to candidates in surplus transfers
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "per_ballot"], default_value="single_step", value_name="mode")] #[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "per_ballot"], default_value="single_step", value_name="mode")]
sum_surplus_transfers: String, sum_surplus_transfers: String,
@ -122,15 +122,15 @@ struct STV {
#[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")] #[clap(help_heading=Some("STV VARIANTS"), short='s', long, possible_values=&["wig", "uig", "eg", "meek"], default_value="wig", value_name="method")]
surplus: String, surplus: String,
/// Order to distribute surpluses /// (Gregory STV) Order to distribute surpluses
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")] #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["by_size", "by_order"], default_value="by_size", value_name="order")]
surplus_order: String, surplus_order: String,
/// Examine only transferable papers during surplus distributions /// (Gregory STV) Examine only transferable papers during surplus distributions
#[clap(help_heading=Some("STV VARIANTS"), long)] #[clap(help_heading=Some("STV VARIANTS"), long)]
transferable_only: bool, transferable_only: bool,
/// Method of exclusions /// (Gregory STV) Method of exclusions
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")] #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
exclusion: String, exclusion: String,

View File

@ -745,6 +745,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
} else { } else {
if opts.early_bulk_elect && state.num_elected + 1 == state.election.seats { if opts.early_bulk_elect && state.num_elected + 1 == state.election.seats {
// Early bulk election and one seat remains: VRE is majority of total active vote // Early bulk election and one seat remains: VRE is majority of total active vote
// FIXME: This probably conflicts with constraints in some cases
update_vre(state, opts); update_vre(state, opts);
} else { } else {
// No use of VRE // No use of VRE
@ -1005,6 +1006,7 @@ where
if opts.early_bulk_elect { if opts.early_bulk_elect {
// Determine if the proposed exclusion would enable a bulk election // Determine if the proposed exclusion would enable a bulk election
// See comment in exclude_hopefuls as to constraints
if can_bulk_elect(state, excluded_candidates.len()) { if can_bulk_elect(state, excluded_candidates.len()) {
// Exclude candidates without further transfers // Exclude candidates without further transfers
let order_excluded = state.num_excluded + 1; let order_excluded = state.num_excluded + 1;
@ -1106,6 +1108,7 @@ where
if opts.early_bulk_elect { if opts.early_bulk_elect {
// Determine if the proposed exclusion would enable a bulk election // Determine if the proposed exclusion would enable a bulk election
// This should be OK for constraints, as if the election of the remaining candidates would be invalid, the excluded candidate must necessarily have be guarded already
if can_bulk_elect(state, excluded_candidates.len()) { if can_bulk_elect(state, excluded_candidates.len()) {
// Exclude candidates without further transfers // Exclude candidates without further transfers
let order_excluded = state.num_excluded + 1; let order_excluded = state.num_excluded + 1;