Implement --subtract-nontransferable for NSW Local Government rules

This commit is contained in:
RunasSudo 2022-03-25 02:46:30 +11:00
parent 4119a293b1
commit 26d45cac50
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
9 changed files with 27 additions and 7 deletions

View File

@ -104,10 +104,11 @@ Random sample methods are also supported, but also not recommended:
The use of a random sample method requires *Normalise ballots* to be enabled, and will usually be used with a *Quota criterion* set to *>=*. The use of a random sample method requires *Normalise ballots* to be enabled, and will usually be used with a *Quota criterion* set to *>=*.
### Papers to examine in surplus transfer (--transferable-only) ### Papers to examine in surplus transfer (--transferable-only/--subtract-nontransferable)
* *Include non-transferable papers* (default): When this option is selected, all ballot papers of the transferring candidate are examined. Non-transferable papers are always exhausted at the relevant surplus fractions. This is the method typically used with the weighted inclusive Gregory or Meek methods. * *Include non-transferable papers* (default): When this option is selected, all ballot papers of the transferring candidate are examined. Non-transferable papers are always exhausted at the relevant surplus fractions. This is the method typically used with the weighted inclusive Gregory or Meek methods.
* *Use transferable papers only*: When this option is selected, only transferable papers of the transferring candidate are examined. Non-transferable papers are exhausted only if the value of the transferable papers is less than the surplus. This is the method typically used with other surplus distribution methods. * *Use transferable papers only* (--transferable-only): When this option is selected, only transferable papers of the transferring candidate are examined. Non-transferable papers are exhausted only if the value of the transferable papers is less than the surplus. This is the method typically used with other surplus distribution methods.
* *Subtract non-transferables* (--transferable-only --subtract-nontransferable): Same as *Use transferable papers only*, but the value of the transferable papers is calculated by subtracting the value of non-transferable papers from the progress total. This has effect only as far as concerns rounding.
### (Gregory) Exclusion method (--exclusion) ### (Gregory) Exclusion method (--exclusion)

View File

@ -123,6 +123,7 @@
<select id="selPapers"> <select id="selPapers">
<option value="both" selected>Include non-transferable papers</option> <option value="both" selected>Include non-transferable papers</option>
<option value="transferable">Use transferable papers only</option> <option value="transferable">Use transferable papers only</option>
<option value="subtract_nontransferable">Subtract non-transferables</option>
</select> </select>
</label> </label>
</div> </div>

View File

@ -154,7 +154,7 @@ async function clickCount() {
document.getElementById('txtSeed').value, document.getElementById('txtSeed').value,
document.getElementById('selMethod').value, document.getElementById('selMethod').value,
document.getElementById('selSurplus').value, document.getElementById('selSurplus').value,
document.getElementById('selPapers').value == 'transferable', document.getElementById('selPapers').value,
document.getElementById('selExclusion').value, document.getElementById('selExclusion').value,
document.getElementById('chkMeekNZExclusion').checked, document.getElementById('chkMeekNZExclusion').checked,
document.getElementById('selSample').value, document.getElementById('selSample').value,

View File

@ -239,7 +239,7 @@ function changePreset() {
document.getElementById('selSumTransfers').value = 'by_value_and_source'; document.getElementById('selSumTransfers').value = 'by_value_and_source';
document.getElementById('selSurplus').value = 'by_order'; document.getElementById('selSurplus').value = 'by_order';
document.getElementById('selMethod').value = 'wig'; document.getElementById('selMethod').value = 'wig';
document.getElementById('selPapers').value = 'transferable'; document.getElementById('selPapers').value = 'subtract_nontransferable';
document.getElementById('selExclusion').value = 'single_stage'; document.getElementById('selExclusion').value = 'single_stage';
document.getElementById('selTies').value = 'backwards,random'; document.getElementById('selTies').value = 'backwards,random';
} else if (document.getElementById('selPreset').value === 'minneapolis') { } else if (document.getElementById('selPreset').value === 'minneapolis') {

View File

@ -125,6 +125,10 @@ pub struct SubcmdOptions {
#[clap(help_heading=Some("STV VARIANTS"), long)] #[clap(help_heading=Some("STV VARIANTS"), long)]
transferable_only: bool, transferable_only: bool,
/// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers
#[clap(help_heading=Some("STV VARIANTS"), long)]
subtract_nontransferable: bool,
/// (Gregory STV) Method of exclusions /// (Gregory STV) Method of exclusions
#[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")] #[clap(help_heading=Some("STV VARIANTS"), long, possible_values=&["single_stage", "by_value", "by_source", "parcels_by_order", "wright"], default_value="single_stage", value_name="method")]
exclusion: String, exclusion: String,
@ -294,6 +298,7 @@ where
cmd_opts.surplus.into(), cmd_opts.surplus.into(),
cmd_opts.surplus_order.into(), cmd_opts.surplus_order.into(),
cmd_opts.transferable_only, cmd_opts.transferable_only,
cmd_opts.subtract_nontransferable,
cmd_opts.exclusion.into(), cmd_opts.exclusion.into(),
cmd_opts.meek_nz_exclusion, cmd_opts.meek_nz_exclusion,
cmd_opts.sample.into(), cmd_opts.sample.into(),

View File

@ -265,6 +265,11 @@ where
let count_card = state.candidates.get_mut(elected_candidate).unwrap(); let count_card = state.candidates.get_mut(elected_candidate).unwrap();
count_card.ballot_transfers = -&total_ballots; count_card.ballot_transfers = -&total_ballots;
if opts.transferable_only && opts.subtract_nontransferable {
// Override transferable_votes
transferable_votes = count_card.votes.clone() - exhausted_votes;
}
let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts); let mut surplus_denom = calculate_surplus_denom(&surplus, &transferable_ballots, &transferable_votes, &total_ballots, &total_votes, opts);
let surplus_numer; let surplus_numer;
let mut surplus_fraction; let mut surplus_fraction;

View File

@ -104,6 +104,10 @@ pub struct STVOptions {
#[builder(default="false")] #[builder(default="false")]
pub transferable_only: bool, pub transferable_only: bool,
/// (Gregory STV) If --transferable-only, calculate value of transferable papers by subtracting value of non-transferable papers
#[builder(default="false")]
pub subtract_nontransferable: bool,
/// (Gregory STV) Method of exclusions /// (Gregory STV) Method of exclusions
#[builder(default="ExclusionMethod::SingleStage")] #[builder(default="ExclusionMethod::SingleStage")]
pub exclusion: ExclusionMethod, pub exclusion: ExclusionMethod,
@ -189,6 +193,7 @@ impl STVOptions {
if self.surplus != SurplusMethod::Meek { if self.surplus != SurplusMethod::Meek {
if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); } if self.surplus_order != SurplusOrder::BySize { flags.push(self.surplus_order.describe()); }
if self.transferable_only { flags.push("--transferable-only".to_string()); } if self.transferable_only { flags.push("--transferable-only".to_string()); }
if self.subtract_nontransferable { flags.push("--subtract-nontransferable".to_string()); }
if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); } if self.exclusion != ExclusionMethod::SingleStage { flags.push(self.exclusion.describe()); }
} }
if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); } if self.surplus == SurplusMethod::Meek && self.meek_nz_exclusion { flags.push("--meek-nz-exclusion".to_string()); }
@ -224,6 +229,7 @@ impl STVOptions {
//if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); } //if self.sample == SampleMethod::StratifyFloor && self.sample_per_ballot { return Err(STVError::InvalidOptions("--sample stratify_floor is incompatible with --sample-per-ballot")); }
if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); } if self.sample_per_ballot && !self.immediate_elect { return Err(STVError::InvalidOptions("--sample-per-ballot is incompatible with --no-immediate-elect")); }
} }
if self.subtract_nontransferable && !self.transferable_only { return Err(STVError::InvalidOptions("--subtract-nontransferable requires --transferable-only")) }
if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI if self.min_threshold != "0" && self.defer_surpluses { return Err(STVError::InvalidOptions("--min-threshold is incompatible with --defer-surpluses (not yet implemented)")); } // TODO: NYI
if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude { return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)")); } // TODO: NYI if self.round_subtransfers == RoundSubtransfersMode::ByValueAndSource && self.bulk_exclude { return Err(STVError::InvalidOptions("--round-subtransfers by_value_and_source is incompatible with --bulk-exclude (not yet implemented)")); } // TODO: NYI
return Ok(()); return Ok(());

View File

@ -249,7 +249,7 @@ impl STVOptions {
random_seed: String, random_seed: String,
surplus: &str, surplus: &str,
surplus_order: &str, surplus_order: &str,
transferable_only: bool, papers: &str,
exclusion: &str, exclusion: &str,
meek_nz_exclusion: bool, meek_nz_exclusion: bool,
sample: &str, sample: &str,
@ -277,7 +277,8 @@ impl STVOptions {
ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)), ties::from_strs(ties.iter().map(|v| v.as_string().unwrap()).collect(), Some(random_seed)),
surplus.into(), surplus.into(),
surplus_order.into(), surplus_order.into(),
transferable_only, if papers == "transferable" || papers == "subtract_nontransferable" { true } else { false },
if papers == "subtract_nontransferable" { true } else { false },
exclusion.into(), exclusion.into(),
meek_nz_exclusion, meek_nz_exclusion,
sample.into(), sample.into(),

View File

@ -33,9 +33,10 @@ fn nswlg_albury21_rational() {
.ties(vec![TieStrategy::Backwards, TieStrategy::Random(String::from("20220322"))]) .ties(vec![TieStrategy::Backwards, TieStrategy::Random(String::from("20220322"))])
.surplus_order(stv::SurplusOrder::ByOrder) .surplus_order(stv::SurplusOrder::ByOrder)
.transferable_only(true) .transferable_only(true)
.subtract_nontransferable(true)
.build().unwrap(); .build().unwrap();
assert_eq!(stv_opts.describe::<Rational>(), "--round-votes 0 --round-quota 0 --round-subtransfers by_value_and_source --quota-criterion geq --ties backwards random --random-seed 20220322 --surplus-order by_order --transferable-only"); assert_eq!(stv_opts.describe::<Rational>(), "--round-votes 0 --round-quota 0 --round-subtransfers by_value_and_source --quota-criterion geq --ties backwards random --random-seed 20220322 --surplus-order by_order --transferable-only --subtract-nontransferable");
utils::read_validate_election::<Rational>("tests/data/City_of_Albury-finalpreferencedatafile.csv", "tests/data/City_of_Albury-finalpreferencedatafile.blt", stv_opts, None, &[]); utils::read_validate_election::<Rational>("tests/data/City_of_Albury-finalpreferencedatafile.csv", "tests/data/City_of_Albury-finalpreferencedatafile.blt", stv_opts, None, &[]);
} }