Implement --round-subtransfers by_parcel for NSW Local Government rules
This commit is contained in:
parent
26d45cac50
commit
df9223ebe6
|
@ -32,7 +32,7 @@ Exceptions:
|
|||
* [E2] When breaking ties backwards, OpenTally selects the candidate who had more/fewer votes at the last stage when *any* tied candidate had more/fewer votes, rather than the method described in the legislation (when each all had unequal votes). The OpenTally developers regard the method described in the legislation as a defect. For an independent discussion, see <a href="https://dl.acm.org/doi/10.1145/3014812.3014837">Conway et al.</a>
|
||||
* [E3] A tie between 2 candidates for the final vacancy will be broken backwards then at random, rather than the method described in the legislation.
|
||||
* [E4] Bulk exclusion is not performed, as the prescribed rules are more conservative than OpenTally's. See also the section on *Bulk exclusion* for further discussion.
|
||||
* [E5] The legislation specifies that, when rounding subtransfers, surpluses are to be transferred ‘by value’ while exclusions are to be transferred ‘by value and source’. One imagines this differing treatment was a drafting oversight, and the New South Wales Electoral Commission has instead applied the ‘by value and source’ method for both in practice, which OpenTally follows. See also <a href="https://github.com/AndrewConway/ConcreteSTV/blob/main/nsw/NSWLocalCouncilLegislation2021Commentary.md">Conway</a>.
|
||||
* [E5] The legislation is drafted such that a consistent interpretation is impossible – see <a href="https://github.com/AndrewConway/ConcreteSTV/blob/main/nsw/NSWLocalCouncilLegislation2021Commentary.md">Conway</a> for a discussion. In practice, the New South Wales Electoral Commission has applied the ‘by parcel’ method of rounding subtransfers, which OpenTally follows.
|
||||
* [E6] The ‘mathematically eliminated by the sum of all ranked-choice votes comparison’ is not implemented.
|
||||
* [E7] The ‘quarter of a quota’ provision is disregarded when determining whether to defer surplus distributions.
|
||||
* [E8] No distinction is made between stages and substages (during exclusion). This affects only the numbering of stages and not the result.
|
||||
|
@ -281,6 +281,7 @@ When *Surplus method* is set to a Gregory method, this option allows you to spec
|
|||
* *Single step* (default): The total value of all votes expressing a next available preference for that candidate is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *By value*: The votes expressing a next available preference for that candidate are further divided according to value. For each group of votes at a particular value, the total value of all such votes is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *By value and source*: The votes are further divided according to value, and according to who they were received from by the elected/excluded candidate. Then as per *By value*.
|
||||
* *By parcel*: For each parcel of votes, the total value of the votes in the parcel expressing a next available preference for that candidate is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
* *Per ballot*: For each individual vote expressing a next available preference for that candidate, the value of the vote is multiplied by the surplus fraction. The product (rounded if requested) is credited to that candidate.
|
||||
|
||||
This option affects the result only as far as rounding (due to use of fixed-precision/floating-point arithmetic, or an explicit rounding option) is concerned.
|
||||
|
|
|
@ -301,6 +301,7 @@
|
|||
<option value="single_step" selected>Single step</option>
|
||||
<option value="by_value">By value</option>
|
||||
<option value="by_value_and_source">By value and source</option>
|
||||
<option value="by_parcel">By parcel</option>
|
||||
<option value="per_ballot">Per ballot</option>
|
||||
</select>
|
||||
</label>
|
||||
|
|
|
@ -236,7 +236,7 @@ function changePreset() {
|
|||
document.getElementById('txtRoundVotes').value = '0';
|
||||
document.getElementById('chkRoundSFs').checked = false;
|
||||
document.getElementById('chkRoundValues').checked = false;
|
||||
document.getElementById('selSumTransfers').value = 'by_value_and_source';
|
||||
document.getElementById('selSumTransfers').value = 'by_parcel';
|
||||
document.getElementById('selSurplus').value = 'by_order';
|
||||
document.getElementById('selMethod').value = 'wig';
|
||||
document.getElementById('selPapers').value = 'subtract_nontransferable';
|
||||
|
|
|
@ -80,7 +80,7 @@ pub struct SubcmdOptions {
|
|||
round_quota: Option<usize>,
|
||||
|
||||
/// (Gregory STV) How to round subtransfers during surpluses/exclusions
|
||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "by_value_and_source", "per_ballot"], default_value="single_step", value_name="mode")]
|
||||
#[clap(help_heading=Some("ROUNDING"), long, possible_values=&["single_step", "by_value", "by_value_and_source", "by_parcel", "per_ballot"], default_value="single_step", value_name="mode")]
|
||||
round_subtransfers: String,
|
||||
|
||||
/// (Meek STV) Limit for stopping iteration of surplus distribution
|
||||
|
|
|
@ -326,7 +326,11 @@ where
|
|||
// Record transfers
|
||||
transfer_table.add_transfers(
|
||||
&value_fraction,
|
||||
if opts.round_subtransfers == RoundSubtransfersMode::ByValueAndSource { Some(source_order) } else { None },
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(source_order),
|
||||
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
||||
_ => Some(0)
|
||||
},
|
||||
candidate,
|
||||
&entry.num_ballots
|
||||
);
|
||||
|
@ -363,7 +367,11 @@ where
|
|||
// Record exhausted votes
|
||||
transfer_table.add_exhausted(
|
||||
&value_fraction,
|
||||
if opts.round_subtransfers == RoundSubtransfersMode::ByValueAndSource { Some(source_order) } else { None },
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(source_order),
|
||||
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
||||
_ => Some(0)
|
||||
},
|
||||
&result.exhausted.num_ballots
|
||||
);
|
||||
|
||||
|
@ -603,7 +611,11 @@ where
|
|||
// Record transfers
|
||||
transfer_table.add_transfers(
|
||||
&parcel.value_fraction,
|
||||
if opts.round_subtransfers == RoundSubtransfersMode::ByValueAndSource { Some(src_parcel.source_order) } else { None },
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order),
|
||||
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
||||
_ => Some(0)
|
||||
},
|
||||
candidate,
|
||||
&entry.num_ballots
|
||||
);
|
||||
|
@ -622,7 +634,11 @@ where
|
|||
// Record transfers
|
||||
transfer_table.add_exhausted(
|
||||
&parcel.value_fraction,
|
||||
if opts.round_subtransfers == RoundSubtransfersMode::ByValueAndSource { Some(src_parcel.source_order) } else { None },
|
||||
match opts.round_subtransfers {
|
||||
RoundSubtransfersMode::ByValueAndSource => Some(src_parcel.source_order),
|
||||
RoundSubtransfersMode::ByParcel => None, // Force new column per parcel
|
||||
_ => Some(0)
|
||||
},
|
||||
&result.exhausted.num_ballots
|
||||
);
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
columns: Vec::new(),
|
||||
total: TransferTableColumn {
|
||||
value_fraction: N::new(),
|
||||
source_order: None,
|
||||
order: 0,
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
|
@ -75,7 +75,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
columns: Vec::new(),
|
||||
total: TransferTableColumn {
|
||||
value_fraction: N::new(),
|
||||
source_order: None,
|
||||
order: 0,
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
|
@ -88,9 +88,11 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
}
|
||||
|
||||
/// Record the specified transfer
|
||||
pub fn add_transfers(&mut self, value_fraction: &N, source_order: Option<usize>, candidate: &'e Candidate, ballots: &N) {
|
||||
///
|
||||
/// order: Pass `None` to force a new column
|
||||
pub fn add_transfers(&mut self, value_fraction: &N, order: Option<usize>, candidate: &'e Candidate, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction && col.source_order == source_order {
|
||||
if &col.value_fraction == value_fraction && order.map(|o| col.order == o).unwrap_or(false) {
|
||||
col.add_transfers(candidate, ballots);
|
||||
return;
|
||||
}
|
||||
|
@ -98,7 +100,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
|
||||
let mut col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
source_order: source_order,
|
||||
order: order.unwrap_or(0),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
|
@ -108,9 +110,11 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
}
|
||||
|
||||
/// Record the specified exhaustion
|
||||
pub fn add_exhausted(&mut self, value_fraction: &N, source_order: Option<usize>, ballots: &N) {
|
||||
///
|
||||
/// order: Pass `None` to force a new column
|
||||
pub fn add_exhausted(&mut self, value_fraction: &N, order: Option<usize>, ballots: &N) {
|
||||
for col in self.columns.iter_mut() {
|
||||
if &col.value_fraction == value_fraction && col.source_order == source_order {
|
||||
if &col.value_fraction == value_fraction && order.map(|o| col.order == o).unwrap_or(false) {
|
||||
col.exhausted.ballots += ballots;
|
||||
return;
|
||||
}
|
||||
|
@ -118,7 +122,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
|
||||
let col = TransferTableColumn {
|
||||
value_fraction: value_fraction.clone(),
|
||||
source_order: source_order,
|
||||
order: order.unwrap_or(0),
|
||||
cells: HashMap::new(),
|
||||
exhausted: TransferTableCell { ballots: ballots.clone(), votes_in: N::new(), votes_out: N::new() },
|
||||
total: TransferTableCell { ballots: N::new(), votes_in: N::new(), votes_out: N::new() },
|
||||
|
@ -203,7 +207,7 @@ impl<'e, N: Number> TransferTable<'e, N> {
|
|||
self.total.exhausted.votes_out.floor_mut(dps);
|
||||
}
|
||||
}
|
||||
RoundSubtransfersMode::ByValue | RoundSubtransfersMode::ByValueAndSource => {
|
||||
RoundSubtransfersMode::ByValue | RoundSubtransfersMode::ByValueAndSource | RoundSubtransfersMode::ByParcel => {
|
||||
// Calculate votes_out for each column
|
||||
for column in self.columns.iter_mut() {
|
||||
// Calculate votes_out per candidate in the column
|
||||
|
@ -565,8 +569,8 @@ pub struct TransferTableColumn<'e, N: Number> {
|
|||
/// Value fraction of ballots counted in this column
|
||||
pub value_fraction: N,
|
||||
|
||||
/// Number to separate parcels in modes where subtransfers by parcel, or `None` if unused
|
||||
pub source_order: Option<usize>,
|
||||
/// Number to separate parcels in modes where subtransfers by parcel, etc.
|
||||
pub order: usize,
|
||||
|
||||
/// Cells in this column
|
||||
pub cells: HashMap<&'e Candidate, TransferTableCell<N>>,
|
||||
|
|
|
@ -247,6 +247,8 @@ pub enum RoundSubtransfersMode {
|
|||
ByValue,
|
||||
/// Round in subtransfers according to the candidate from who each vote was received, and the value when received
|
||||
ByValueAndSource,
|
||||
/// Round in subtransfers according to parcel
|
||||
ByParcel,
|
||||
/// Sum and round transfers individually for each ballot paper
|
||||
PerBallot,
|
||||
}
|
||||
|
@ -258,6 +260,7 @@ impl RoundSubtransfersMode {
|
|||
RoundSubtransfersMode::SingleStep => "--round-subtransfers single_step",
|
||||
RoundSubtransfersMode::ByValue => "--round-subtransfers by_value",
|
||||
RoundSubtransfersMode::ByValueAndSource => "--round-subtransfers by_value_and_source",
|
||||
RoundSubtransfersMode::ByParcel => "--round-subtransfers by_parcel",
|
||||
RoundSubtransfersMode::PerBallot => "--round-subtransfers per_ballot",
|
||||
}.to_string()
|
||||
}
|
||||
|
@ -269,8 +272,9 @@ impl<S: AsRef<str>> From<S> for RoundSubtransfersMode {
|
|||
"single_step" => RoundSubtransfersMode::SingleStep,
|
||||
"by_value" => RoundSubtransfersMode::ByValue,
|
||||
"by_value_and_source" => RoundSubtransfersMode::ByValueAndSource,
|
||||
"by_parcel" => RoundSubtransfersMode::ByParcel,
|
||||
"per_ballot" => RoundSubtransfersMode::PerBallot,
|
||||
_ => panic!("Invalid --sum-transfers"),
|
||||
_ => panic!("Invalid --round-subtransfers"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ fn nswlg_albury21_rational() {
|
|||
let stv_opts = stv::STVOptionsBuilder::default()
|
||||
.round_votes(Some(0))
|
||||
.round_quota(Some(0))
|
||||
.round_subtransfers(stv::RoundSubtransfersMode::ByValueAndSource)
|
||||
.round_subtransfers(stv::RoundSubtransfersMode::ByParcel)
|
||||
.quota_criterion(stv::QuotaCriterion::GreaterOrEqual)
|
||||
.ties(vec![TieStrategy::Backwards, TieStrategy::Random(String::from("20220322"))])
|
||||
.surplus_order(stv::SurplusOrder::ByOrder)
|
||||
|
@ -36,7 +36,7 @@ fn nswlg_albury21_rational() {
|
|||
.subtract_nontransferable(true)
|
||||
.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 --subtract-nontransferable");
|
||||
assert_eq!(stv_opts.describe::<Rational>(), "--round-votes 0 --round-quota 0 --round-subtransfers by_parcel --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, &[]);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue