Implement dynamic quotas
This commit is contained in:
parent
ee1008b509
commit
ae0d1d8411
|
@ -63,9 +63,11 @@ Note that the combination ‘*>= Droop (exact)*’ (with *Round quota to [n] d.p
|
|||
This option allows you to specify whether the votes required for election can change during the count. The options are:
|
||||
|
||||
* *Static quota*: The quota is calculated once after all first-preference votes are allocated, and remains constant throughout the count.
|
||||
* *Static with ERS97 rules*: The quota is static, but candidates may be elected if their vote exceeds (or equals, according to the *Quota criterion*) the total active vote, divided by (*S* + 1) (or *S*, according to the *Quota* option).
|
||||
* *Static with ERS97 rules*: The quota is static, but candidates may be elected if their vote exceeds (or equals, according to the *Quota criterion*) the active vote, divided by (*S* + 1).
|
||||
* *Dynamic by total vote*: The quota is recalculated at the end of each stage, according to the *Quota* option.
|
||||
* *Dynamic by active vote*: The quota is recalculated at the end of each stage, according to the *Quota* option, but where *V* is the active vote and *S* is the number of remaining vacancies.
|
||||
|
||||
When *Surplus method* is set to *Meek method*, this setting is ignored, and the progressively reducing quota of the Meek method is instead applied.
|
||||
When a dynamic quota is used, then unless *Surplus method* is set to *Meek*, the quota that applies to an elected candidate is the quota at the start of the stage when the candidate's surplus is distributed. Further distributions are not performed later, even if the quota is later reduced.
|
||||
|
||||
## STV variants
|
||||
|
||||
|
|
|
@ -94,6 +94,8 @@
|
|||
<!--<option value="progressive">Progressive quota</option>-->
|
||||
<option value="ers97">Static with ERS97 rules</option>
|
||||
<option value="ers76">Static with ERS76 rules</option>
|
||||
<option value="dynamic_by_total">Dynamic by total vote</option>
|
||||
<option value="dynamic_by_active">Dynamic by active vote</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -413,7 +413,7 @@ function changePreset() {
|
|||
} else if (document.getElementById('selPreset').value === 'meek87') {
|
||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||
document.getElementById('selQuota').value = 'droop_exact';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = false;
|
||||
|
@ -438,7 +438,7 @@ function changePreset() {
|
|||
} else if (document.getElementById('selPreset').value === 'meek06') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
|
@ -467,7 +467,7 @@ function changePreset() {
|
|||
} else if (document.getElementById('selPreset').value === 'meeknz') {
|
||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||
document.getElementById('selQuota').value = 'droop';
|
||||
document.getElementById('selQuotaMode').value = 'static';
|
||||
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||
document.getElementById('chkBulkElection').checked = true;
|
||||
document.getElementById('chkBulkExclusion').checked = false;
|
||||
document.getElementById('chkDeferSurpluses').checked = true;
|
||||
|
|
|
@ -105,7 +105,7 @@ struct STV {
|
|||
quota_criterion: String,
|
||||
|
||||
/// Whether to apply a form of progressive quota
|
||||
#[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97", "ers76"], default_value="static", value_name="mode")]
|
||||
#[clap(help_heading=Some("QUOTA"), long, possible_values=&["static", "ers97", "ers76", "dynamic_by_total", "dynamic_by_active"], default_value="static", value_name="mode")]
|
||||
quota_mode: String,
|
||||
|
||||
// ------------------
|
||||
|
|
|
@ -206,6 +206,7 @@ impl STVOptions {
|
|||
/// Validate the combination of [STVOptions] and panic if invalid
|
||||
pub fn validate(&self) -> Result<(), STVError> {
|
||||
if self.surplus == SurplusMethod::Meek {
|
||||
if self.quota_mode != QuotaMode::DynamicByTotal { return Err(STVError::InvalidOptions("--surplus meek requires --quota-mode dynamic_by_total")); }
|
||||
if self.transferable_only { return Err(STVError::InvalidOptions("--surplus meek is incompatible with --transferable-only")); }
|
||||
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); }
|
||||
}
|
||||
|
@ -336,6 +337,10 @@ pub enum QuotaMode {
|
|||
ERS97,
|
||||
/// Static quota with ERS76 rules
|
||||
ERS76,
|
||||
/// Dynamic quota by total vote
|
||||
DynamicByTotal,
|
||||
/// Dynamic quota by active vote
|
||||
DynamicByActive,
|
||||
}
|
||||
|
||||
impl QuotaMode {
|
||||
|
@ -345,6 +350,8 @@ impl QuotaMode {
|
|||
QuotaMode::Static => "--quota-mode static",
|
||||
QuotaMode::ERS97 => "--quota-mode ers97",
|
||||
QuotaMode::ERS76 => "--quota-mode ers76",
|
||||
QuotaMode::DynamicByTotal => "--quota-mode dynamic_by_total",
|
||||
QuotaMode::DynamicByActive => "--quota-mode dynamic_by_active",
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
|
@ -355,6 +362,8 @@ impl<S: AsRef<str>> From<S> for QuotaMode {
|
|||
"static" => QuotaMode::Static,
|
||||
"ers97" => QuotaMode::ERS97,
|
||||
"ers76" => QuotaMode::ERS76,
|
||||
"dynamic_by_total" => QuotaMode::DynamicByTotal,
|
||||
"dynamic_by_active" => QuotaMode::DynamicByActive,
|
||||
_ => panic!("Invalid --quota-mode"),
|
||||
}
|
||||
}
|
||||
|
@ -780,16 +789,17 @@ fn total_to_quota<N: Number>(mut total: N, seats: usize, opts: &STVOptions) -> N
|
|||
fn update_vre<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
let mut log = String::new();
|
||||
|
||||
// Calculate total active vote
|
||||
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||||
// Calculate active vote
|
||||
let active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||||
match cc.state {
|
||||
CandidateState::Elected => { if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
||||
_ => { acc + &cc.votes }
|
||||
}
|
||||
});
|
||||
log.push_str(format!("Total active vote is {:.dps$}, so the vote required for election is ", total_active_vote, dps=opts.pp_decimals).as_str());
|
||||
log.push_str(format!("Active vote is {:.dps$}, so the vote required for election is ", active_vote, dps=opts.pp_decimals).as_str());
|
||||
|
||||
let vote_req = total_active_vote / N::from(state.election.seats - state.num_elected + 1);
|
||||
// TODO: Calculate according to --quota ?
|
||||
let vote_req = active_vote / N::from(state.election.seats - state.num_elected + 1);
|
||||
|
||||
if &vote_req < state.quota.as_ref().unwrap() {
|
||||
// VRE is less than the quota
|
||||
|
@ -812,8 +822,9 @@ fn update_vre<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||
|
||||
/// Calculate the quota according to [STVOptions::quota]
|
||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||
// Calculate quota
|
||||
if state.quota.is_none() || opts.surplus == SurplusMethod::Meek {
|
||||
if state.quota.is_none() || opts.quota_mode == QuotaMode::DynamicByTotal {
|
||||
// Calculate quota by total vote
|
||||
|
||||
let mut log = String::new();
|
||||
|
||||
// Calculate the total vote
|
||||
|
@ -822,6 +833,27 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||
|
||||
let quota = total_to_quota(total_vote, state.election.seats, opts);
|
||||
|
||||
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||||
state.quota = Some(quota);
|
||||
state.logger.log_literal(log);
|
||||
|
||||
} else if opts.quota_mode == QuotaMode::DynamicByActive {
|
||||
// Calculate quota by active vote
|
||||
|
||||
let mut log = String::new();
|
||||
|
||||
// Calculate the active vote
|
||||
let active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||||
match cc.state {
|
||||
CandidateState::Elected => { if !cc.finalised && &cc.votes > state.quota.as_ref().unwrap() { acc + &cc.votes - state.quota.as_ref().unwrap() } else { acc } }
|
||||
_ => { acc + &cc.votes }
|
||||
}
|
||||
});
|
||||
log.push_str(format!("Active vote is {:.dps$}, so the quota is is ", active_vote, dps=opts.pp_decimals).as_str());
|
||||
|
||||
// TODO: Calculate according to --quota ?
|
||||
let quota = active_vote / N::from(state.election.seats - state.num_elected + 1);
|
||||
|
||||
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||||
state.quota = Some(quota);
|
||||
state.logger.log_literal(log);
|
||||
|
@ -856,13 +888,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
|||
update_vre(state, opts);
|
||||
}
|
||||
} else {
|
||||
// No ERS97/ERS76 rules
|
||||
if opts.surplus == SurplusMethod::Meek {
|
||||
// Update quota and so VRE every stage
|
||||
state.vote_required_election = state.quota.clone();
|
||||
} else {
|
||||
// No use of VRE
|
||||
}
|
||||
// No ERS97/ERS76 rules, so no use of VRE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -881,13 +907,8 @@ fn cmp_quota_criterion<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &S
|
|||
/// Determine if the given candidate meets the vote required to be elected, according to [STVOptions::quota_criterion] and [STVOptions::quota_mode]
|
||||
fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
||||
if let Some(vre) = &state.vote_required_election {
|
||||
if opts.quota_mode == QuotaMode::ERS97 || opts.quota_mode == QuotaMode::ERS76 {
|
||||
// VRE is set because ERS97/ERS76 rules
|
||||
return cmp_quota_criterion(vre, count_card, opts);
|
||||
} else {
|
||||
// VRE is set because early bulk election is enabled and 1 vacancy remains
|
||||
return count_card.votes > *vre;
|
||||
}
|
||||
// VRE is set because ERS97/ERS76 rules
|
||||
return cmp_quota_criterion(vre, count_card, opts);
|
||||
} else {
|
||||
return cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ fn meek87_ers97_float64() {
|
|||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.meek_surplus_tolerance("0.001%".to_string())
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.quota_mode(stv::QuotaMode::DynamicByTotal)
|
||||
.surplus(stv::SurplusMethod::Meek)
|
||||
.immediate_elect(false)
|
||||
.build().unwrap();
|
||||
|
@ -45,6 +46,7 @@ fn meek06_ers97_fixed12() {
|
|||
.meek_surplus_tolerance("0.0001".to_string())
|
||||
.quota(stv::QuotaType::Droop)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.quota_mode(stv::QuotaMode::DynamicByTotal)
|
||||
.surplus(stv::SurplusMethod::Meek)
|
||||
.defer_surpluses(true)
|
||||
.build().unwrap();
|
||||
|
@ -104,6 +106,7 @@ fn meeknz_ers97_fixed12() {
|
|||
.meek_surplus_tolerance("0.0001".to_string())
|
||||
.quota(stv::QuotaType::Droop)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.quota_mode(stv::QuotaMode::DynamicByTotal)
|
||||
.surplus(stv::SurplusMethod::Meek)
|
||||
.meek_nz_exclusion(true)
|
||||
.defer_surpluses(true)
|
||||
|
|
Loading…
Reference in New Issue