Implement --quota, --quota-criterion
This commit is contained in:
parent
f6fba85049
commit
e88d2674d6
15
src/main.rs
15
src/main.rs
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue