Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI and config updates for Stop Tabulation Early #636

Merged
merged 5 commits into from
Mar 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/main/java/network/brightspots/rcv/ContestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class ContestConfig {
private static final int MAX_ROW_INDEX = 100000;
private static final int MIN_MAX_RANKINGS_ALLOWED = 1;
private static final int MIN_MAX_SKIPPED_RANKS_ALLOWED = 0;
private static final int MIN_NUMBER_OF_ROUNDS = 0;
private static final int MIN_NUMBER_OF_WINNERS = 0;
private static final int MIN_DECIMAL_PLACES_FOR_VOTE_ARITHMETIC = 1;
private static final int MAX_DECIMAL_PLACES_FOR_VOTE_ARITHMETIC = 20;
Expand Down Expand Up @@ -701,6 +702,15 @@ && getMaxRankingsAllowed() < MIN_MAX_RANKINGS_ALLOWED)) {
ValidationError.RULES_MULTI_SEAT_BOTTOMS_UP_PERCENTAGE_THRESHOLD_INVALID);
}

if (fieldOutOfRangeOrNotInteger(
getStopTabulationEarlyOnRoundRaw(),
"stopEarlyOnRound",
MIN_NUMBER_OF_ROUNDS,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be 1? If they say to stop after 0 rounds, that's the same as just not running the tabulation at all, isn't it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I figured that might be useful in some cases -- you'd still get the output files with zero rounds (similar to running a Zero Report?). I can change it to 1 though if you prefer.

Integer.MAX_VALUE,
false)) {
validationErrors.add(ValidationError.RULES_STOP_TABULATION_EARLY_ON_ROUND_INVALID);
}

WinnerElectionMode winnerMode = getWinnerElectionMode();
if (Utils.isInt(getNumberOfWinnersRaw())) {
if (getNumberOfWinners() > 0) {
Expand Down Expand Up @@ -789,6 +799,10 @@ private String getNumberOfWinnersRaw() {
return rawConfig.rules.numberOfWinners;
}

private String getStopTabulationEarlyOnRoundRaw() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, this setting means that we'd stop after the specified number of rounds, right? If so, let's call it that instead to make it more clear. Stop on a round makes it sound like maybe we're stopping at the beginning of that round.

return rawConfig.rules.stopTabulationEarlyOnRound;
}

Integer getNumberOfWinners() {
return Integer.parseInt(getNumberOfWinnersRaw());
}
Expand Down Expand Up @@ -947,6 +961,12 @@ boolean isContinueUntilTwoCandidatesRemainEnabled() {
return rawConfig.rules.continueUntilTwoCandidatesRemain;
}

Integer getStopTabulationEarlyOnRound() {
return isNullOrBlank(getStopTabulationEarlyOnRoundRaw())
? Integer.MAX_VALUE
: Integer.parseInt(getStopTabulationEarlyOnRoundRaw());
}

int getNumDeclaredCandidates() {
int size = getCandidateCodeList().size();
if (undeclaredWriteInsEnabled()) {
Expand Down Expand Up @@ -1125,6 +1145,7 @@ enum ValidationError {
RULES_MIN_DECIMAL_PLACES_FOR_VOTE_ARITHMETIC_INVALID,
RULES_MIN_VOTE_THRESHOLD_INVALID,
RULES_MULTI_SEAT_BOTTOMS_UP_PERCENTAGE_THRESHOLD_INVALID,
RULES_STOP_TABULATION_EARLY_ON_ROUND_INVALID,
RULES_NUMBER_OF_WINNERS_INVALID_FOR_WINNER_ELECTION_MODE,
RULES_CONTINUE_UNTIL_TWO_CANDIDATES_REMAIN_TRUE_FOR_MULTI_SEAT,
RULES_BATCH_ELIMINATION_TRUE_FOR_MULTI_SEAT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ static void migrateConfigVersion(ContestConfig config)
}
}

// Migrations from 1.3.0 to 1.4.0
if (rules.stopTabulationEarlyOnRound == null) {
rules.stopTabulationEarlyOnRound = "";
}

Logger.info(
"Migrated tabulator config version from %s to %s.",
config.rawConfig.tabulatorVersion != null ? config.rawConfig.tabulatorVersion : "unknown",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ public class GuiConfigController implements Initializable {
@FXML
private CheckBox checkBoxContinueUntilTwoCandidatesRemain;
@FXML
private TextField textFieldStopTabulationEarlyOnRound;
@FXML
private CheckBox checkBoxExhaustOnDuplicateCandidate;
@FXML
private MenuBar menuBar;
Expand Down Expand Up @@ -795,6 +797,8 @@ private void clearAndDisableWinningRuleFields() {
checkBoxMaxRankingsAllowedMax.setDisable(true);
textFieldMinimumVoteThreshold.clear();
textFieldMinimumVoteThreshold.setDisable(true);
textFieldStopTabulationEarlyOnRound.clear();
textFieldStopTabulationEarlyOnRound.setDisable(true);
checkBoxBatchElimination.setSelected(false);
checkBoxBatchElimination.setDisable(true);
checkBoxContinueUntilTwoCandidatesRemain.setSelected(false);
Expand Down Expand Up @@ -1110,6 +1114,7 @@ public LocalDate fromString(String string) {
setWinningRulesDefaultValues();
checkBoxMaxRankingsAllowedMax.setDisable(false);
textFieldMinimumVoteThreshold.setDisable(false);
textFieldStopTabulationEarlyOnRound.setDisable(false);
choiceTiebreakMode.setDisable(false);
switch (getWinnerElectionModeChoice(choiceWinnerElectionMode)) {
case STANDARD_SINGLE_WINNER -> {
Expand Down Expand Up @@ -1250,6 +1255,7 @@ private void loadConfig(ContestConfig config) throws ConfigVersionIsNewerThanApp
setThresholdCalculationMethodRadioButton(rules.nonIntegerWinningThreshold, rules.hareQuota);
checkBoxBatchElimination.setSelected(rules.batchElimination);
checkBoxContinueUntilTwoCandidatesRemain.setSelected(rules.continueUntilTwoCandidatesRemain);
textFieldStopTabulationEarlyOnRound.setText(rules.stopTabulationEarlyOnRound);
checkBoxExhaustOnDuplicateCandidate.setSelected(rules.exhaustOnDuplicateCandidate);
}

Expand Down Expand Up @@ -1340,6 +1346,7 @@ private RawContestConfig createRawContestConfig() {
rules.hareQuota = radioThresholdHareQuota.isSelected();
rules.batchElimination = checkBoxBatchElimination.isSelected();
rules.continueUntilTwoCandidatesRemain = checkBoxContinueUntilTwoCandidatesRemain.isSelected();
rules.stopTabulationEarlyOnRound = getTextOrEmptyString(textFieldStopTabulationEarlyOnRound);
rules.exhaustOnDuplicateCandidate = checkBoxExhaustOnDuplicateCandidate.isSelected();
rules.rulesDescription = getTextOrEmptyString(textFieldRulesDescription);
config.rules = rules;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/network/brightspots/rcv/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
public class Main extends GuiApplication {

public static final String APP_NAME = "RCTab";
public static final String APP_VERSION = "1.3.0";
public static final String APP_VERSION = "1.4.0.alpha";

/**
* Main entry point to RCTab.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ public static class ContestRules {
public boolean hareQuota;
public boolean batchElimination;
public boolean continueUntilTwoCandidatesRemain;
public String stopTabulationEarlyOnRound;
public boolean exhaustOnDuplicateCandidate;
public String rulesDescription;

Expand Down
4 changes: 3 additions & 1 deletion src/main/java/network/brightspots/rcv/Tabulator.java
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,9 @@ private boolean shouldContinueTabulating() {
int numEliminatedCandidates = candidateToRoundEliminated.size();
int numWinnersDeclared = winnerToRound.size();
// apply config setting if specified
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's just delete this comment; it's ambiguous and doesn't add anything anyway

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do. (Note: It wasn't part of this PR.)

if (config.isContinueUntilTwoCandidatesRemainEnabled()) {
if (currentRound >= config.getStopTabulationEarlyOnRound()) {
keepTabulating = false;
} else if (config.isContinueUntilTwoCandidatesRemainEnabled()) {
// Keep going if there are more than two candidates alive. Also make sure we tabulate one last
// round after we've made our final elimination.
keepTabulating = numEliminatedCandidates + numWinnersDeclared + 1 < config.getNumCandidates()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,13 @@
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
</padding>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="4.0">
<Label prefWidth="220.0" text="Stop Tabulation Early on Round"/>
<TextField fx:id="textFieldStopTabulationEarlyOnRound" maxWidth="80.0" />
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
</padding>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="4.0">
<CheckBox fx:id="checkBoxBatchElimination" mnemonicParsing="false"
text="Use Batch Elimination">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Use Batch Elimination: Batch elimination, or simultaneous elimination of all can

Continue until Two Candidates Remain: Single-winner ranked-choice voting elections can stop as soon as a candidate receives a majority of votes, even though 3 or more candidates may still be in the race. Selecting this option will run the round-by-round count until only two candidates remain, regardless of when a candidate wins a majority of votes. Available only when Winner Election Mode is "Single-winner majority determines winner" or "Multi-pass IRV."

Stop Tabulation Early on Round: If a winner is not found by the given round, tabulation stops early.

Tiebreak Mode (required): Ties in ranked-choice voting contests can occur when eliminating candidates or when electing candidates. Multi-winner contests can have ties between candidates who have both crossed the threshold of election; in that case ties are broken to determine whose surplus vote value transfers first. Tiebreak procedures are set in law, either in the ranked-choice voting law used in your jurisdiction or in the elections code more generally. Select the option from this list that complies with law and procedure in your jurisdiction.

* Random: Randomly select a tied candidate to eliminate or, in multi-winner contests only, elect. Requires a random seed.
Expand Down
6 changes: 6 additions & 0 deletions src/test/java/network/brightspots/rcv/TabulatorTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ void testSkipToNext() {
runTabulationTest("skip_to_next_test");
}

@Test
@DisplayName("test stopping tabulation early")
void testStopTabulationEarly() {
runTabulationTest("stop_tabulation_early_test");
}

@Test
@DisplayName("test Hare quota")
void testHareQuota() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
artoonie marked this conversation as resolved.
Show resolved Hide resolved
"tabulatorVersion": "TEST",
"outputSettings": {
"contestName": "Stop Tabulation Early Test",
"outputDirectory": "output",
"contestDate": "2023-03-14",
"contestJurisdiction": "Funkytown, USA",
"contestOffice": "Sergeant-at-Arms",
"tabulateByPrecinct": false,
"generateCdfJson": false
},
"cvrFileSources": [
{
"filePath" : "stop_tabulation_early_test_cvr.xlsx",
"firstVoteColumnIndex" : "2",
"firstVoteRowIndex" : "2",
"idColumnIndex" : "1",
"precinctColumnIndex" : "",
"provider" : "ess",
"treatBlankAsUndeclaredWriteIn": false,
"overvoteLabel": "overvote",
"undervoteLabel": "undervote",
"undeclaredWriteInLabel": ""
} ],
"candidates" : [ {
"name" : "Mookie Blaylock",
"code" : "",
"excluded" : false
}, {
"name" : "Yinka Dare",
"code" : "",
"excluded" : false
}, {
"name" : "George Gervin",
"code" : "",
"excluded" : false
} ],
"rules" : {
"tiebreakMode": "random",
"overvoteRule": "alwaysSkipToNextRank",
"winnerElectionMode": "singleWinnerMajority",
"randomSeed": "1",
"numberOfWinners": "1",
"decimalPlacesForVoteArithmetic": "4",
"minimumVoteThreshold": "0",
"maxSkippedRanksAllowed": "0",
"maxRankingsAllowed": "3",
"nonIntegerWinningThreshold": false,
"hareQuota": false,
"batchElimination": true,
"exhaustOnDuplicateCandidate": false,
"rulesDescription": "Doyle Rules",
"stopTabulationEarlyOnRound": "2"
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"config" : {
"contest" : "Stop Tabulation Early Test",
"date" : "2023-03-14",
"jurisdiction" : "Funkytown, USA",
"office" : "Sergeant-at-Arms",
"threshold" : "4"
},
"results" : [ {
"round" : 1,
"tally" : {
"George Gervin" : "3",
"Mookie Blaylock" : "3",
"Yinka Dare" : "3"
},
"tallyResults" : [ {
"eliminated" : "Yinka Dare",
"transfers" : {
"exhausted" : "3"
}
} ]
}, {
"round" : 2,
"tally" : {
"George Gervin" : "3",
"Mookie Blaylock" : "3"
},
"tallyResults" : [ {
"eliminated" : "George Gervin",
"transfers" : { }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the legislation, having no transfers on this round seems correct. Let me know if anybody disagrees. (It doesn't make sense to add in last-round transfers to me, but I did want to flag just in case.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to answer this. @tarheel?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to agree, since transfers would only be relevant if you were going to then proceed and show the tallies for the next round. But @chughes297 and @rrojas350 may have an opinion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, you'd only show the results of the surplus transfer in the next round.

} ]
} ]
}