Implement --quota, --quota-criterion

This commit is contained in:
RunasSudo 2021-06-02 18:07:05 +10:00
parent f6fba85049
commit e88d2674d6
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 121 additions and 20 deletions

View File

@ -77,11 +77,22 @@ 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>,
// -----------
// -- Quota --
/// Quota type
#[clap(help_heading=Some("QUOTA"), short, long, possible_values=&["droop", "hare", "droop_exact", "hare_exact"], default_value="droop_exact")]
quota: String,
/// Whether to elect candidates on meeting (geq) or strictly exceeding (gt) the quota
#[clap(help_heading=Some("QUOTA"), short='c', long, possible_values=&["geq", "gt"], default_value="gt", value_name="criterion")]
quota_criterion: String,
// ------------------ // ------------------
// -- STV variants -- // -- STV variants --
/// Method of surplus transfers /// Method of surplus transfers
#[clap(help_heading=Some("STV VARIANTS"), 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,
#[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")]
@ -142,6 +153,8 @@ where
cmd_opts.round_weights, cmd_opts.round_weights,
cmd_opts.round_votes, cmd_opts.round_votes,
cmd_opts.round_quota, cmd_opts.round_quota,
&cmd_opts.quota,
&cmd_opts.quota_criterion,
&cmd_opts.surplus, &cmd_opts.surplus,
&cmd_opts.surplus_order, &cmd_opts.surplus_order,
cmd_opts.transferable_only, cmd_opts.transferable_only,

View File

@ -47,6 +47,7 @@ where
fn pow_assign(&mut self, exponent: i32); fn pow_assign(&mut self, exponent: i32);
fn floor_mut(&mut self, dps: usize); fn floor_mut(&mut self, dps: usize);
fn ceil_mut(&mut self, dps: usize);
fn parse(s: &str) -> Self { fn parse(s: &str) -> Self {
if let Ok(value) = Self::from_str_radix(s, 10) { if let Ok(value) = Self::from_str_radix(s, 10) {

View File

@ -39,6 +39,11 @@ impl Number for NativeFloat64 {
let factor = 10.0_f64.powi(dps as i32); let factor = 10.0_f64.powi(dps as i32);
self.0 = (self.0 * factor).floor() / factor; self.0 = (self.0 * factor).floor() / factor;
} }
fn ceil_mut(&mut self, dps: usize) {
let factor = 10.0_f64.powi(dps as i32);
self.0 = (self.0 * factor).ceil() / factor;
}
} }
impl Num for NativeFloat64 { impl Num for NativeFloat64 {

View File

@ -44,6 +44,17 @@ impl Number for Rational {
self.0 /= factor; self.0 /= factor;
} }
} }
fn ceil_mut(&mut self, dps: usize) {
if dps == 0 {
self.0 = self.0.ceil();
} else {
let factor = BigRational::from_integer(BigInt::from(10)).pow(dps as i32);
self.0 *= &factor;
self.0 = self.0.ceil();
self.0 /= factor;
}
}
} }
impl Num for Rational { impl Num for Rational {

View File

@ -44,6 +44,17 @@ impl Number for Rational {
self.0 /= factor; self.0 /= factor;
} }
} }
fn ceil_mut(&mut self, dps: usize) {
if dps == 0 {
self.0.ceil_mut();
} else {
let factor = rug::Rational::from(10).pow(dps as u32);
self.0 *= &factor;
self.0.ceil_mut();
self.0 /= factor;
}
}
} }
impl Num for Rational { impl Num for Rational {

View File

@ -34,6 +34,8 @@ pub struct STVOptions {
pub round_weights: Option<usize>, pub round_weights: Option<usize>,
pub round_votes: Option<usize>, pub round_votes: Option<usize>,
pub round_quota: Option<usize>, pub round_quota: Option<usize>,
pub quota: QuotaType,
pub quota_criterion: QuotaCriterion,
pub surplus: SurplusMethod, pub surplus: SurplusMethod,
pub surplus_order: SurplusOrder, pub surplus_order: SurplusOrder,
pub transferable_only: bool, pub transferable_only: bool,
@ -48,6 +50,8 @@ impl STVOptions {
round_weights: Option<usize>, round_weights: Option<usize>,
round_votes: Option<usize>, round_votes: Option<usize>,
round_quota: Option<usize>, round_quota: Option<usize>,
quota: &str,
quota_criterion: &str,
surplus: &str, surplus: &str,
surplus_order: &str, surplus_order: &str,
transferable_only: bool, transferable_only: bool,
@ -59,6 +63,18 @@ impl STVOptions {
round_weights, round_weights,
round_votes, round_votes,
round_quota, round_quota,
quota: match quota {
"droop" => QuotaType::Droop,
"hare" => QuotaType::Hare,
"droop_exact" => QuotaType::DroopExact,
"hare_exact" => QuotaType::HareExact,
_ => panic!("Invalid --quota"),
},
quota_criterion: match quota_criterion {
"geq" => QuotaCriterion::GreaterOrEqual,
"gt" => QuotaCriterion::Greater,
_ => panic!("Invalid --quota-criterion"),
},
surplus: match surplus { surplus: match surplus {
"wig" => SurplusMethod::WIG, "wig" => SurplusMethod::WIG,
"uig" => SurplusMethod::UIG, "uig" => SurplusMethod::UIG,
@ -83,6 +99,22 @@ impl STVOptions {
} }
} }
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum QuotaType {
Droop,
Hare,
DroopExact,
HareExact,
}
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum QuotaCriterion {
GreaterOrEqual,
Greater,
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum SurplusMethod { pub enum SurplusMethod {
@ -110,7 +142,7 @@ pub enum ExclusionMethod {
pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) { pub fn count_init<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) {
distribute_first_preferences(&mut state); distribute_first_preferences(&mut state);
calculate_quota(&mut state, opts); calculate_quota(&mut state, opts);
elect_meeting_quota(&mut state); elect_meeting_quota(&mut state, opts);
} }
pub fn count_one_stage<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool pub fn count_one_stage<N: Number>(mut state: &mut CountState<'_, N>, opts: &STVOptions) -> bool
@ -129,25 +161,25 @@ where
// Continue exclusions // Continue exclusions
if continue_exclusion(&mut state, &opts) { if continue_exclusion(&mut state, &opts) {
elect_meeting_quota(&mut state); elect_meeting_quota(&mut state, opts);
return false; return false;
} }
// Distribute surpluses // Distribute surpluses
if distribute_surpluses(&mut state, &opts) { if distribute_surpluses(&mut state, &opts) {
elect_meeting_quota(&mut state); elect_meeting_quota(&mut state, opts);
return false; return false;
} }
// Attempt bulk election // Attempt bulk election
if bulk_elect(&mut state) { if bulk_elect(&mut state) {
elect_meeting_quota(&mut state); elect_meeting_quota(&mut state, opts);
return false; return false;
} }
// Exclude lowest hopeful // Exclude lowest hopeful
if exclude_hopefuls(&mut state, &opts) { if exclude_hopefuls(&mut state, &opts) {
elect_meeting_quota(&mut state); elect_meeting_quota(&mut state, opts);
return false; return false;
} }
@ -256,11 +288,19 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes }); state.quota = state.candidates.values().fold(N::zero(), |acc, cc| { acc + &cc.votes });
log.push_str(format!("{:.dps$} usable votes, so the quota is ", state.quota, dps=opts.pp_decimals).as_str()); log.push_str(format!("{:.dps$} usable votes, so the quota is ", state.quota, dps=opts.pp_decimals).as_str());
// TODO: Different quotas match opts.quota {
QuotaType::Droop | QuotaType::DroopExact => {
state.quota /= N::from(state.election.seats + 1); state.quota /= N::from(state.election.seats + 1);
}
QuotaType::Hare | QuotaType::HareExact => {
state.quota /= N::from(state.election.seats);
}
}
// Increment to next available increment
if let Some(dps) = opts.round_quota { if let Some(dps) = opts.round_quota {
match opts.quota {
QuotaType::Droop | QuotaType::Hare => {
// Increment to next available increment
let mut factor = N::from(10); let mut factor = N::from(10);
factor.pow_assign(dps as i32); factor.pow_assign(dps as i32);
state.quota *= &factor; state.quota *= &factor;
@ -268,21 +308,37 @@ fn calculate_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
state.quota += N::one(); state.quota += N::one();
state.quota /= factor; state.quota /= factor;
} }
QuotaType::DroopExact | QuotaType::HareExact => {
// Round up to next available increment if necessary
let mut factor = N::from(10);
factor.pow_assign(dps as i32);
state.quota *= &factor;
state.quota.ceil_mut(0);
state.quota /= factor;
}
}
}
log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str()); log.push_str(format!("{:.dps$}.", state.quota, dps=opts.pp_decimals).as_str());
state.logger.log_literal(log); state.logger.log_literal(log);
} }
fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>) -> bool { fn meets_quota<N: Number>(quota: &N, count_card: &CountCard<N>, opts: &STVOptions) -> bool {
// TODO: Different quota rules match opts.quota_criterion {
QuotaCriterion::GreaterOrEqual => {
return count_card.votes >= *quota; return count_card.votes >= *quota;
} }
QuotaCriterion::Greater => {
return count_card.votes > *quota;
}
}
}
fn elect_meeting_quota<N: Number>(state: &mut CountState<N>) { fn elect_meeting_quota<N: Number>(state: &mut CountState<N>, opts: &STVOptions) {
let quota = &state.quota; // Have to do this or else the borrow checker gets confused let quota = &state.quota; // Have to do this or else the borrow checker gets confused
let mut cands_meeting_quota: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut() let mut cands_meeting_quota: Vec<(&&Candidate, &mut CountCard<N>)> = state.candidates.iter_mut()
.filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc)) .filter(|(_, cc)| cc.state == CandidateState::HOPEFUL && meets_quota(quota, cc, opts))
.collect(); .collect();
if cands_meeting_quota.len() > 0 { if cands_meeting_quota.len() > 0 {

View File

@ -59,6 +59,8 @@ fn aec_tas19_rational() {
round_weights: None, round_weights: None,
round_votes: Some(0), round_votes: Some(0),
round_quota: Some(0), round_quota: Some(0),
quota: stv::QuotaType::Droop,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
surplus: stv::SurplusMethod::UIG, surplus: stv::SurplusMethod::UIG,
surplus_order: stv::SurplusOrder::ByOrder, surplus_order: stv::SurplusOrder::ByOrder,
transferable_only: false, transferable_only: false,

View File

@ -27,6 +27,8 @@ fn prsa1_rational() {
round_weights: Some(3), round_weights: Some(3),
round_votes: Some(3), round_votes: Some(3),
round_quota: Some(3), round_quota: Some(3),
quota: stv::QuotaType::Droop,
quota_criterion: stv::QuotaCriterion::GreaterOrEqual,
surplus: stv::SurplusMethod::EG, surplus: stv::SurplusMethod::EG,
surplus_order: stv::SurplusOrder::ByOrder, surplus_order: stv::SurplusOrder::ByOrder,
transferable_only: true, transferable_only: true,