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:
|
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 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
|
## STV variants
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,8 @@
|
||||||
<!--<option value="progressive">Progressive quota</option>-->
|
<!--<option value="progressive">Progressive quota</option>-->
|
||||||
<option value="ers97">Static with ERS97 rules</option>
|
<option value="ers97">Static with ERS97 rules</option>
|
||||||
<option value="ers76">Static with ERS76 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>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -413,7 +413,7 @@ function changePreset() {
|
||||||
} else if (document.getElementById('selPreset').value === 'meek87') {
|
} else if (document.getElementById('selPreset').value === 'meek87') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'gt';
|
document.getElementById('selQuotaCriterion').value = 'gt';
|
||||||
document.getElementById('selQuota').value = 'droop_exact';
|
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('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = false;
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
document.getElementById('chkDeferSurpluses').checked = false;
|
document.getElementById('chkDeferSurpluses').checked = false;
|
||||||
|
@ -438,7 +438,7 @@ function changePreset() {
|
||||||
} else if (document.getElementById('selPreset').value === 'meek06') {
|
} else if (document.getElementById('selPreset').value === 'meek06') {
|
||||||
document.getElementById('selQuotaCriterion').value = 'geq';
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
||||||
document.getElementById('selQuota').value = 'droop';
|
document.getElementById('selQuota').value = 'droop';
|
||||||
document.getElementById('selQuotaMode').value = 'static';
|
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = false;
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
document.getElementById('chkDeferSurpluses').checked = true;
|
document.getElementById('chkDeferSurpluses').checked = true;
|
||||||
|
@ -467,7 +467,7 @@ function changePreset() {
|
||||||
} 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';
|
||||||
document.getElementById('selQuotaMode').value = 'static';
|
document.getElementById('selQuotaMode').value = 'dynamic_by_total';
|
||||||
document.getElementById('chkBulkElection').checked = true;
|
document.getElementById('chkBulkElection').checked = true;
|
||||||
document.getElementById('chkBulkExclusion').checked = false;
|
document.getElementById('chkBulkExclusion').checked = false;
|
||||||
document.getElementById('chkDeferSurpluses').checked = true;
|
document.getElementById('chkDeferSurpluses').checked = true;
|
||||||
|
|
|
@ -105,7 +105,7 @@ struct STV {
|
||||||
quota_criterion: String,
|
quota_criterion: String,
|
||||||
|
|
||||||
/// Whether to apply a form of progressive quota
|
/// 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,
|
quota_mode: String,
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
|
|
|
@ -206,6 +206,7 @@ impl STVOptions {
|
||||||
/// Validate the combination of [STVOptions] and panic if invalid
|
/// Validate the combination of [STVOptions] and panic if invalid
|
||||||
pub fn validate(&self) -> Result<(), STVError> {
|
pub fn validate(&self) -> Result<(), STVError> {
|
||||||
if self.surplus == SurplusMethod::Meek {
|
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.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")); }
|
if self.exclusion != ExclusionMethod::SingleStage { return Err(STVError::InvalidOptions("--surplus meek requires --exclusion single_stage")); }
|
||||||
}
|
}
|
||||||
|
@ -336,6 +337,10 @@ pub enum QuotaMode {
|
||||||
ERS97,
|
ERS97,
|
||||||
/// Static quota with ERS76 rules
|
/// Static quota with ERS76 rules
|
||||||
ERS76,
|
ERS76,
|
||||||
|
/// Dynamic quota by total vote
|
||||||
|
DynamicByTotal,
|
||||||
|
/// Dynamic quota by active vote
|
||||||
|
DynamicByActive,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QuotaMode {
|
impl QuotaMode {
|
||||||
|
@ -345,6 +350,8 @@ impl QuotaMode {
|
||||||
QuotaMode::Static => "--quota-mode static",
|
QuotaMode::Static => "--quota-mode static",
|
||||||
QuotaMode::ERS97 => "--quota-mode ers97",
|
QuotaMode::ERS97 => "--quota-mode ers97",
|
||||||
QuotaMode::ERS76 => "--quota-mode ers76",
|
QuotaMode::ERS76 => "--quota-mode ers76",
|
||||||
|
QuotaMode::DynamicByTotal => "--quota-mode dynamic_by_total",
|
||||||
|
QuotaMode::DynamicByActive => "--quota-mode dynamic_by_active",
|
||||||
}.to_string()
|
}.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,6 +362,8 @@ impl<S: AsRef<str>> From<S> for QuotaMode {
|
||||||
"static" => QuotaMode::Static,
|
"static" => QuotaMode::Static,
|
||||||
"ers97" => QuotaMode::ERS97,
|
"ers97" => QuotaMode::ERS97,
|
||||||
"ers76" => QuotaMode::ERS76,
|
"ers76" => QuotaMode::ERS76,
|
||||||
|
"dynamic_by_total" => QuotaMode::DynamicByTotal,
|
||||||
|
"dynamic_by_active" => QuotaMode::DynamicByActive,
|
||||||
_ => panic!("Invalid --quota-mode"),
|
_ => 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) {
|
fn update_vre<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
let mut log = String::new();
|
let mut log = String::new();
|
||||||
|
|
||||||
// Calculate total active vote
|
// Calculate active vote
|
||||||
let total_active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
let active_vote = state.candidates.values().fold(N::zero(), |acc, cc| {
|
||||||
match cc.state {
|
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 } }
|
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 }
|
_ => { 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() {
|
if &vote_req < state.quota.as_ref().unwrap() {
|
||||||
// VRE is less than the quota
|
// 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]
|
/// Calculate the quota according to [STVOptions::quota]
|
||||||
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
// Calculate quota
|
if state.quota.is_none() || opts.quota_mode == QuotaMode::DynamicByTotal {
|
||||||
if state.quota.is_none() || opts.surplus == SurplusMethod::Meek {
|
// Calculate quota by total vote
|
||||||
|
|
||||||
let mut log = String::new();
|
let mut log = String::new();
|
||||||
|
|
||||||
// Calculate the total vote
|
// 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);
|
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());
|
log.push_str(format!("{:.dps$}.", quota, dps=opts.pp_decimals).as_str());
|
||||||
state.quota = Some(quota);
|
state.quota = Some(quota);
|
||||||
state.logger.log_literal(log);
|
state.logger.log_literal(log);
|
||||||
|
@ -856,13 +888,7 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
|
||||||
update_vre(state, opts);
|
update_vre(state, opts);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No ERS97/ERS76 rules
|
// No ERS97/ERS76 rules, so no use of VRE
|
||||||
if opts.surplus == SurplusMethod::Meek {
|
|
||||||
// Update quota and so VRE every stage
|
|
||||||
state.vote_required_election = state.quota.clone();
|
|
||||||
} else {
|
|
||||||
// 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]
|
/// 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 {
|
fn meets_vre<N: Number>(state: &CountState<N>, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
|
||||||
if let Some(vre) = &state.vote_required_election {
|
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
|
||||||
// VRE is set because ERS97/ERS76 rules
|
return cmp_quota_criterion(vre, count_card, opts);
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return cmp_quota_criterion(state.quota.as_ref().unwrap(), count_card, opts);
|
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()
|
let stv_opts = stv::STVOptionsBuilder::default()
|
||||||
.meek_surplus_tolerance("0.001%".to_string())
|
.meek_surplus_tolerance("0.001%".to_string())
|
||||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||||
|
.quota_mode(stv::QuotaMode::DynamicByTotal)
|
||||||
.surplus(stv::SurplusMethod::Meek)
|
.surplus(stv::SurplusMethod::Meek)
|
||||||
.immediate_elect(false)
|
.immediate_elect(false)
|
||||||
.build().unwrap();
|
.build().unwrap();
|
||||||
|
@ -45,6 +46,7 @@ fn meek06_ers97_fixed12() {
|
||||||
.meek_surplus_tolerance("0.0001".to_string())
|
.meek_surplus_tolerance("0.0001".to_string())
|
||||||
.quota(stv::QuotaType::Droop)
|
.quota(stv::QuotaType::Droop)
|
||||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||||
|
.quota_mode(stv::QuotaMode::DynamicByTotal)
|
||||||
.surplus(stv::SurplusMethod::Meek)
|
.surplus(stv::SurplusMethod::Meek)
|
||||||
.defer_surpluses(true)
|
.defer_surpluses(true)
|
||||||
.build().unwrap();
|
.build().unwrap();
|
||||||
|
@ -104,6 +106,7 @@ fn meeknz_ers97_fixed12() {
|
||||||
.meek_surplus_tolerance("0.0001".to_string())
|
.meek_surplus_tolerance("0.0001".to_string())
|
||||||
.quota(stv::QuotaType::Droop)
|
.quota(stv::QuotaType::Droop)
|
||||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||||
|
.quota_mode(stv::QuotaMode::DynamicByTotal)
|
||||||
.surplus(stv::SurplusMethod::Meek)
|
.surplus(stv::SurplusMethod::Meek)
|
||||||
.meek_nz_exclusion(true)
|
.meek_nz_exclusion(true)
|
||||||
.defer_surpluses(true)
|
.defer_surpluses(true)
|
||||||
|
|
Loading…
Reference in New Issue