Implement Wright STV

This commit is contained in:
RunasSudo 2021-06-22 15:23:46 +10:00
parent a1c21cf2b4
commit d46eb69f26
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 123 additions and 22 deletions

View File

@ -41,7 +41,7 @@
<option value="meek06">Meek STV (2006)</option>
<option value="meeknz">Meek STV (New Zealand)</option>
<option value="senate">Australian Senate STV</option>
<!--<option value="wright">Wright STV</option>-->
<option value="wright">Wright STV</option>
<option value="prsa77">PRSA 1977</option>
<option value="ers97">ERS97</option>
</select>
@ -112,7 +112,7 @@
<option value="single_stage" selected>Single stage</option>
<option value="by_value">By value</option>
<option value="parcels_by_order">By parcel (by order)</option>
<!--<option value="wright">Wright method (re-iterate)</option>-->
<option value="wright">Wright method (re-iterate)</option>
</select>
</label>
<label style="margin-left:1em;">

View File

@ -460,6 +460,28 @@ function changePreset() {
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'by_value';
document.getElementById('selTies').value = 'backwards,random';
} else if (document.getElementById('selPreset').value === 'wright') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';
document.getElementById('selQuotaMode').value = 'static';
//document.getElementById('chkBulkElection').checked = true;
document.getElementById('chkBulkExclusion').checked = true;
document.getElementById('chkDeferSurpluses').checked = false;
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 = '0';
document.getElementById('chkRoundVotes').checked = false;
document.getElementById('chkRoundTVs').checked = false;
document.getElementById('chkRoundWeights').checked = false;
document.getElementById('selSumTransfers').value = 'single_step';
document.getElementById('selSurplus').value = 'by_size';
document.getElementById('selTransfers').value = 'wig';
document.getElementById('selPapers').value = 'both';
document.getElementById('selExclusion').value = 'wright';
document.getElementById('selTies').value = 'random';
} else if (document.getElementById('selPreset').value === 'prsa77') {
document.getElementById('selQuotaCriterion').value = 'geq';
document.getElementById('selQuota').value = 'droop';

View File

@ -106,6 +106,10 @@ tr.stage-no td:not(:empty), tr.transfers td {
tr.info:last-child td, .bb {
border-bottom: 1px solid #76858c;
}
.blw {
/* Used to separate counts in Wright STV */
border-left: 2px solid #76858c;
}
/* Table stripes */

View File

@ -63,7 +63,8 @@ where
let quota = state.quota.as_ref().unwrap();
let mut has_surplus: Vec<(&Candidate, &CountCard<N>)> = state.election.candidates.iter() // Present in order in case of tie
.map(|c| (c, state.candidates.get(c).unwrap()))
.filter(|(_, cc)| &cc.votes > quota)
//.filter(|(_, cc)| &cc.votes > quota)
.filter(|(_, cc)| &cc.votes > quota && cc.parcels.iter().any(|p| !p.is_empty()))
.collect();
if !has_surplus.is_empty() {
@ -337,6 +338,8 @@ where
count_card.votes.assign(state.quota.as_ref().unwrap());
checksum -= surplus;
count_card.parcels.clear(); // Mark surpluses as done
// Update loss by fraction
state.loss_fraction.transfer(&-checksum);
}
@ -435,6 +438,7 @@ where
checksum -= &votes_transferred;
count_card.transfer(&-votes_transferred);
}
_ => panic!()
}
if !votes.is_empty() {
@ -494,3 +498,59 @@ where
// Update loss by fraction
state.loss_fraction.transfer(&-checksum);
}
/// Perform one stage of a candidate exclusion according to the Wright method
pub fn wright_exclude_candidates<'a, N: Number>(state: &mut CountState<'a, N>, opts: &STVOptions, excluded_candidates: Vec<&'a Candidate>)
where
for<'r> &'r N: ops::Sub<&'r N, Output=N>,
for<'r> &'r N: ops::Mul<&'r N, Output=N>,
for<'r> &'r N: ops::Div<&'r N, Output=N>,
{
// Used to give bulk excluded candidate the same order_elected
let order_excluded = state.num_excluded + 1;
for excluded_candidate in excluded_candidates.iter() {
let count_card = state.candidates.get_mut(excluded_candidate).unwrap();
// Rust borrow checker is unhappy if we try to put this in exclude_hopefuls ??!
if count_card.state != CandidateState::Excluded {
count_card.state = CandidateState::Excluded;
state.num_excluded += 1;
count_card.order_elected = -(order_excluded as isize);
}
}
// Reset count
for (_, count_card) in state.candidates.iter_mut() {
if count_card.order_elected > 0 {
count_card.order_elected = 0;
}
count_card.parcels.clear();
count_card.votes = N::new();
count_card.transfers = N::new();
count_card.state = match count_card.state {
CandidateState::Withdrawn => CandidateState::Withdrawn,
CandidateState::Excluded => CandidateState::Excluded,
_ => CandidateState::Hopeful,
};
}
state.exhausted.votes = N::new();
state.exhausted.transfers = N::new();
state.loss_fraction.votes = N::new();
state.loss_fraction.transfers = N::new();
state.num_elected = 0;
let orig_title = state.title.clone();
// Redistribute first preferences
super::distribute_first_preferences(state, opts);
state.kind = Some("Exclusion of");
state.title = orig_title;
// Trigger recalculation of quota within stv::count_one_stage
state.quota = None;
state.vote_required_election = None;
}

View File

@ -160,6 +160,7 @@ impl STVOptions {
"single_stage" => ExclusionMethod::SingleStage,
"by_value" => ExclusionMethod::ByValue,
"parcels_by_order" => ExclusionMethod::ParcelsByOrder,
"wright" => ExclusionMethod::Wright,
_ => panic!("Invalid --exclusion"),
},
meek_nz_exclusion,
@ -360,6 +361,8 @@ pub enum ExclusionMethod {
ByValue,
/// Transfer the ballot papers of an excluded candidate parcel by parcel in the order received
ParcelsByOrder,
/// Wright method (re-iterate)
Wright,
}
impl ExclusionMethod {
@ -369,6 +372,7 @@ impl ExclusionMethod {
ExclusionMethod::SingleStage => "--exclusion single_stage",
ExclusionMethod::ByValue => "--exclusion by_value",
ExclusionMethod::ParcelsByOrder => "--exclusion parcels_by_order",
ExclusionMethod::Wright => "--exclusion wright",
}.to_string()
}
}
@ -952,6 +956,8 @@ where
// Exclusion in parts compatible only with Gregory method
gregory::exclude_candidates(state, opts, excluded_candidates);
}
ExclusionMethod::Wright => {
gregory::wright_exclude_candidates(state, opts, excluded_candidates);
}
}
}

View File

@ -285,43 +285,52 @@ fn init_results_table<N: Number>(election: &Election<N>, opts: &stv::STVOptions)
/// Generate subsequent columns of the HTML results table
fn update_results_table<N: Number>(stage_num: usize, state: &CountState<N>, opts: &stv::STVOptions) -> Array {
let result = Array::new();
result.push(&format!(r#"<td>{}</td>"#, stage_num).into());
result.push(&format!(r#"<td>{}</td>"#, state.kind.unwrap_or("")).into());
result.push(&format!(r#"<td>{}</td>"#, state.title).into());
// Insert borders to left of new exclusions in Wright STV
let mut tdclasses1 = "";
let mut tdclasses2 = "";
if opts.exclusion == stv::ExclusionMethod::Wright && state.kind == Some("Exclusion of") {
tdclasses1 = r#" class="blw""#;
tdclasses2 = r#"blw "#;
}
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, stage_num).into());
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.kind.unwrap_or("")).into());
result.push(&format!(r#"<td{}>{}</td>"#, tdclasses1, state.title).into());
for candidate in state.election.candidates.iter() {
let count_card = state.candidates.get(candidate).unwrap();
if count_card.state == stv::CandidateState::Elected {
result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count elected">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count elected">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
} else if count_card.state == stv::CandidateState::Excluded {
result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
if count_card.votes.is_zero() {
result.push(&r#"<td class="count excluded">Ex</td>"#.into());
result.push(&format!(r#"<td class="{}count excluded">Ex</td>"#, tdclasses2).into());
} else {
result.push(&format!(r#"<td class="count excluded">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count excluded">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
}
} else if count_card.state == stv::CandidateState::Withdrawn {
result.push(&r#"<td class="count excluded"></td>"#.into());
result.push(&r#"<td class="count excluded">WD</td>"#.into());
result.push(&format!(r#"<td class="{}count excluded"></td>"#, tdclasses2).into());
result.push(&format!(r#"<td class="{}count excluded">WD</td>"#, tdclasses2).into());
} else {
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&count_card.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&count_card.votes, opts.pp_decimals)).into());
}
}
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.exhausted.votes, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.transfers, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&state.loss_fraction.votes, opts.pp_decimals)).into());
// Calculate total votes
let mut total_vote = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
total_vote += &state.exhausted.votes;
total_vote += &state.loss_fraction.votes;
result.push(&format!(r#"<td class="count">{}</td>"#, pp(&total_vote, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(&total_vote, opts.pp_decimals)).into());
result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.quota.as_ref().unwrap(), opts.pp_decimals)).into());
if opts.quota_mode == stv::QuotaMode::ERS97 {
result.push(&format!(r#"<td class="count">{}</td>"#, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
result.push(&format!(r#"<td class="{}count">{}</td>"#, tdclasses2, pp(state.vote_required_election.as_ref().unwrap(), opts.pp_decimals)).into());
}
return result;