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

Demo1 add audit step #231

Merged
merged 16 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ae2d5bb
Added start to fleshing out the audit step in the Demo1 workflow.
michelleblom Nov 29, 2024
95a1de4
Continued to flesh audit the audited ballot upload step for a county …
michelleblom Nov 29, 2024
bc21889
Merge branch 'main' into Demo1-AddAuditStep
michelleblom Dec 5, 2024
0f27042
Added a getRawChoices() method to IRVBallotInterpretation.java for te…
michelleblom Dec 6, 2024
cdaf696
A bit of refactoring of the audit step in Workflow.java, working for …
michelleblom Dec 6, 2024
7916a65
Added check at end of Demo1 workflow to check that ballots remaining …
michelleblom Dec 6, 2024
e0037f6
Added sanity checking method for assertion generation during the work…
michelleblom Dec 6, 2024
b408143
Removed unused imports.
michelleblom Dec 6, 2024
f79725a
Adjusted how we deal with the case that isCovering(CVR) does not hold…
michelleblom Dec 12, 2024
02cf253
Constructed version of Boulder 2023 IRV manifest to match redacted CVRs.
michelleblom Dec 13, 2024
c395f6f
Add synthetic manifest which excludes redacted votes, to avoid phanto…
vteague Dec 13, 2024
42da56c
Reframed verifyAssertions method to VerifySampleSize.
michelleblom Dec 13, 2024
6e087db
Removed discrepancies from demo1-assertions.sql, as the discrepancy c…
michelleblom Dec 13, 2024
c1625ce
Completed the Demo1 workflow using rest assured. The Boulder CSVs und…
michelleblom Dec 20, 2024
b3047ef
Merge branch 'main' into Demo1-AddAuditStep
michelleblom Dec 20, 2024
ca59ca5
Deleted demo properties temporary file.
michelleblom Dec 20, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public IRVComparisonAudit(final ContestResult contestResult, final BigDecimal ri
// As the super class invokes sample size estimation methods in its constructor,
// we need to invoke them here after the audit's assertions have been populated. Otherwise,
// they will not have been computed correctly for IRV.
my_optimistic_recalculate_needed = true;
optimisticSamplesToAudit();
estimatedSamplesToAudit();

Expand Down Expand Up @@ -365,7 +366,7 @@ public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRe
return OptionalInt.empty();
} else {
final Integer maxDiscrepancy = max(discrepancies);
LOGGER.debug(String.format("%s Contest %s: Maximum discrepancy of %d found for CVR ID %d.",
LOGGER.info(String.format("%s Contest %s: Maximum discrepancy of %d found for CVR ID %d.",
prefix, contestName, maxDiscrepancy, cvr.id()));
return OptionalInt.of(maxDiscrepancy);
}
Expand Down Expand Up @@ -417,7 +418,7 @@ public BigDecimal riskMeasurement() {
}
}

LOGGER.debug(String.format("%s IRVComparisonAudit ID %d for contest %s, risk %f.", prefix,
LOGGER.info(String.format("%s IRVComparisonAudit ID %d for contest %s, risk %f.", prefix,
id(), contestName, risk));
return risk;
}
Expand Down Expand Up @@ -451,30 +452,44 @@ public void removeDiscrepancy(final CVRAuditInfo theRecord, final int theType) {
recordTypeCheck(prefix, theRecord, theType);

try {
LOGGER.debug(String.format("%s removing discrepancy associated with CVR %d (maximum type %d).",
LOGGER.info(String.format("%s removing discrepancy associated with CVR %d (maximum type %d).",
prefix, theRecord.cvr().id(), theType));
// Remove the discrepancy associated with the given CVRAuditInfo (CVR/ACVR pair), if one
// exists, in each of the audit's assertions.
boolean removed = false;
for (Assertion a : assertions) {
boolean removed_a = a.removeDiscrepancy(theRecord);
removed = removed || removed_a;
}

// Update the discrepancy tallies in the base ComparisonAudit class, for reporting purposes,
// and the flag for indicating that a sample size recalculation is needed, *if* we did
// indeed remove a discrepancy from at least one of this audit's assertions.
if(removed) {
// The next check is whether the base classes isCovering(theRecord.id()) method holds.
// In this case, we just want to call the base classes removeDiscrepancy() method so that
// the discrepancy, which would be part of the my_discrepancies list, is removed from
// that list. Note that in this case, the base classes overall discrepancy counts will not
// have included the discrepancy.
if(!isCovering(theRecord.id())){
// Ensure that the base class' record method is called (note that the audit's discrepancy
// counts *should not be changed*, but the discrepancy should be removed from its list of
// discrepancies for reporting purposes).
super.removeDiscrepancy(theRecord, theType);
}
else{
// There is an argument that this should actually be a case where an exception should be
// thrown -- where the audit logic is telling the audit to remove discrepancies that have
// not been prior computed and recorded.
LOGGER.warn(String.format("%s no discrepancies removed.", prefix));
else {
// Remove the discrepancy associated with the given CVRAuditInfo (CVR/ACVR pair), if one
// exists, in each of the audit's assertions.
boolean removed = false;
for (Assertion a : assertions) {
boolean removed_a = a.removeDiscrepancy(theRecord);
removed = removed || removed_a;
}

// Update the discrepancy tallies in the base ComparisonAudit class, for reporting purposes,
// and the flag for indicating that a sample size recalculation is needed, *if* we did
// indeed remove a discrepancy from at least one of this audit's assertions.
if (removed) {
super.removeDiscrepancy(theRecord, theType);
} else {
// There is an argument that this should actually be a case where an exception should be
// thrown -- where the audit logic is telling the audit to remove discrepancies that have
// not been prior computed and recorded. However, the logic of ComparisonAudit does not
// include these defensive checks.
LOGGER.warn(String.format("%s no discrepancies removed.", prefix));
}
}

LOGGER.debug(String.format("%s total number of overstatements (%f), optimistic sample " +
LOGGER.info(String.format("%s total number of overstatements (%f), optimistic sample " +
"recalculate needed (%s), estimated sample recalculate needed (%s),", prefix,
getOverstatements(), my_optimistic_recalculate_needed, my_estimated_recalculate_needed));

Expand Down Expand Up @@ -516,7 +531,7 @@ public void recordDiscrepancy(final CVRAuditInfo theRecord, final int theType) {
// IllegalArgumentException if they are not.
recordTypeCheck(prefix, theRecord, theType);

LOGGER.debug(String.format("%s recording discrepancies for CVR ID %d, max type %d.", prefix,
LOGGER.info(String.format("%s recording discrepancies for CVR ID %d, max type %d.", prefix,
theRecord.id(), theType));

// Check that 'theType' is indeed the maximum discrepancy associated with an assertion in
Expand All @@ -538,13 +553,18 @@ public void recordDiscrepancy(final CVRAuditInfo theRecord, final int theType) {
}

// The next check is whether the base classes isCovering(theRecord.id()) method holds. If it
// doesn't, throw an exception. This valid discrepancy is not going to be counted in the
// doesn't, display a warning. This valid discrepancy is not going to be counted in the
// base class counts (as isCovering()) does not hold, yet there is a discrepancy.
if(!isCovering(theRecord.id())){
final String msg = String.format("%s We have computed a discrepancy for contest %s, CVR %d, " +
"but that CVR is not meant to cover the contest.", prefix, contestName, theRecord.id());
LOGGER.error(msg);
throw new RuntimeException(msg);
"but that CVR does not cover the contest.", prefix, contestName, theRecord.id());
LOGGER.warn(msg);

// Ensure that the base class' record method is called (note that the audit's discrepancy
// counts will not be changed, but the discrepancy will be stored in its list of
// discrepancies for reporting purposes).
super.recordDiscrepancy(theRecord, theType);
return;
}

try {
Expand All @@ -567,7 +587,7 @@ public void recordDiscrepancy(final CVRAuditInfo theRecord, final int theType) {
// and the flag for indicating that a sample size recalculation is needed.
super.recordDiscrepancy(theRecord, theType);

LOGGER.debug(String.format("%s total number of overstatements (%f), optimistic sample " +
LOGGER.info(String.format("%s total number of overstatements (%f), optimistic sample " +
"recalculate needed (%s), estimated sample recalculate needed (%s),", prefix,
getOverstatements(), my_optimistic_recalculate_needed, my_estimated_recalculate_needed));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,8 @@ private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) {
LOGGER.debug(String.format("%s Computing score for audited ballot for CVR ID %d, Assertion ID %d, " +
"contest %s.", prefix, cvrID, id, contestName));

if(auditedCVR.recordType() == RecordType.PHANTOM_BALLOT){
if(auditedCVR.recordType() == RecordType.PHANTOM_BALLOT ||
auditedCVR.recordType() == RecordType.PHANTOM_RECORD_ACVR){
// The audited ballot is missing entirely. Return a worst case audited ballot score of -1.
LOGGER.debug(String.format("%s audited ballot for CVR ID %d is a Phantom Record, " +
"audited ballot score is -1 for Assertion ID %d.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package au.org.democracydevelopers.corla.model.vote;

import java.util.Collections;
import us.freeandfair.corla.model.CastVoteRecord;
import us.freeandfair.corla.model.Contest;
import us.freeandfair.corla.persistence.PersistentEntity;
Expand Down Expand Up @@ -104,6 +105,14 @@ public IRVBallotInterpretation(final Contest contest, final CastVoteRecord.Recor
this.interpretation = orderedChoices;
}

/**
* Returns the raw choices, prior to interpretation, as an immutable list.
* @return The raw choices of this vote, prior to interpretation, as an immutable list.
*/
public List<String> getRawChoices(){
return Collections.unmodifiableList(rawChoices);
}

/**
* Output details of the stored IRV vote interpretation as a String appropriate for a log message.
* @return the data with headers incorporated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,21 +542,6 @@ public void testRemoveInvalidDiscrepancy2(){
ca.removeDiscrepancy(auditInfo, -3);
}

/**
* If we call removeDiscrepancy() on an audit that does not contain any discrepancies, no
* discrepancies will be removed.
*/
@Test
public void testRemoveInvalidDiscrepancy3(){
log(LOGGER, "testRemoveInvalidDiscrepancy3");
IRVComparisonAudit ca = new IRVComparisonAudit(mixedContest2,
AssertionTests.riskLimit3, AuditReason.OPPORTUNISTIC_BENEFITS);

checkDiscrepancies(ca, 0, 0, 0, 0, 0);
ca.removeDiscrepancy(auditInfo, 1);
checkDiscrepancies(ca, 0, 0, 0, 0, 0);
}

/**
* If we call removeDiscrepancy() with a null audit info, an IllegalArgumentException will be
* thrown.
Expand Down Expand Up @@ -780,10 +765,9 @@ private void riskMeasurementCheck(IRVComparisonAudit audit, final List<Pair<Inte
/**
* Discrepancy computation for 'Mixed Contest' with CVR "A","B","C","D" and audited ballot
* "B","A","C","D" and recording of the resulting maximum discrepancy of type 2 in the context
* where the base ComparisonAudit does not "cover" the CVR. An exception should be thrown.
* where the base ComparisonAudit does not "cover" the CVR.
*/
@Test(dataProvider = "AuditedRecordTypes", dataProviderClass = AssertionTests.class,
expectedExceptions = RuntimeException.class)
@Test(dataProvider = "AuditedRecordTypes", dataProviderClass = AssertionTests.class)
public void testComputeRecordDiscrepancyNoCover(final RecordType auditedType){
log(LOGGER, String.format("testComputeRecordDiscrepancyNoCover[%s]", auditedType));
resetMocks(ABCD, BACD, RecordType.UPLOADED, ConsensusValue.YES, auditedType, "Mixed Contest");
Expand All @@ -797,8 +781,11 @@ public void testComputeRecordDiscrepancyNoCover(final RecordType auditedType){
// recordDiscrepancy() and removeDiscrepancy() do.
checkDiscrepancies(ca, 0, 0, 0, 0, 0);

// This call should throw an exception as the CVR ID (1L) is not 'covered' by the audit.
// The CVR ID (1L) is not 'covered' by the audit.
ca.recordDiscrepancy(auditInfo, 2);

// Discrepancy counts should not have been changed.
checkDiscrepancies(ca, 0, 0, 0, 0, 0);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

package au.org.democracydevelopers.corla.workflows;


import java.math.BigDecimal;
import java.util.*;

Expand Down Expand Up @@ -86,7 +87,7 @@ public void runDemo1() throws InterruptedException {
CVRS.add(dataPath + "Demo1/4-archuleta-kempsey-plusByron-4.csv");
CVRS.add(dataPath + "split-Byron/Byron-5.csv");
CVRS.add(dataPath + "split-Byron/Byron-6.csv");
CVRS.add(dataPath + "Demo1/7-boulder-2023-plusByron-7.csv");
CVRS.add(dataPath + "Demo1/7-unredacted-boulder-2023-plusByron-7.csv");

List<String> MANIFESTS = new ArrayList<>();

Expand All @@ -96,7 +97,7 @@ public void runDemo1() throws InterruptedException {
MANIFESTS.add(dataPath + "NewSouthWales21/Kempsey_Mayoral.manifest.csv");
MANIFESTS.add(dataPath + "split-Byron/Byron-5-manifest.csv");
MANIFESTS.add(dataPath + "split-Byron/Byron-6-manifest.csv");
MANIFESTS.add(dataPath + "Boulder23/Boulder-IRV-Manifest.csv");
MANIFESTS.add(dataPath + "Demo1/7-unredacted-Boulder-IRV-Manifest.csv");

for(int i = 8 ; i <= numCounties ; ++i){
CVRS.add(dataPath + "split-Byron/Byron-" + i + ".csv");
Expand Down Expand Up @@ -145,9 +146,11 @@ public void runDemo1() throws InterruptedException {
assertEquals(4, dashboard.getList("generate_assertions_summaries").size());

// 5. Choose targeted contests for audit.
targetContests(Map.of("City of Longmont - Mayor","COUNTY_WIDE_CONTEST",
final Map<String,String> targets = Map.of("City of Longmont - Mayor","COUNTY_WIDE_CONTEST",
"Byron Mayoral", "STATE_WIDE_CONTEST",
"Kempsey Mayoral", "COUNTY_WIDE_CONTEST"));
"Kempsey Mayoral", "COUNTY_WIDE_CONTEST");
final List<String> irvContests = List.of("Byron Mayoral", "Kempsey Mayoral");
targetContests(targets);

// 6. Set the seed.
setSeed(defaultSeed);
Expand All @@ -161,9 +164,49 @@ public void runDemo1() throws InterruptedException {
Map<String, EstimateSampleSizes.EstimateData> sampleSizes = getSampleSizeEstimates();
assertFalse(sampleSizes.isEmpty());

// TODO Sanity check of assertions and sample size estimates.
// Byron should be 3820. Kempsey 332.
// For each targeted contest, check that the set of assertions: (i) is not empty; (ii) has
// the correct minimum diluted margin; and (ii) has resulted in the correct sample size estimate.
Map<String,Double> expectedDilutedMargins = Map.of("City of Longmont - Mayor", 0.09078192,
"Byron Mayoral", 0.00684644, "Kempsey Mayoral", 0.02200739);

for(final String c : targets.keySet()) {
verifySampleSize(c, expectedDilutedMargins.get(c),
sampleSizes.get(c).estimatedSamples(), riskLimit, irvContests.contains(c));
}

// 8. Start Audit Round
startAuditRound();

// 9. Log in as each county, and audit all ballots in sample.
List<TestAuditSession> sessions = new ArrayList<>();
for(final int cty : allCounties){
sessions.add(countyAuditInitialise(cty));
}

// ACVR uploads for each county. Cannot run in parallel as corla does not like
// simultaneous database accesses.
for(final TestAuditSession entry : sessions){
auditCounty(1, entry);
}

// Audit board sign off for each county.
for(final TestAuditSession entry : sessions){
countySignOffLogout(entry);
}

// Check that there are no more ballots to sample across all counties in first round,
// and as this demo involved no discprepancies, that all audits are complete.
dashboard = getDoSDashBoardRefreshResponse();
final Map<String,Map<String,Object>> status = dashboard.get(COUNTY_STATUS);

for(final Map.Entry<String,Map<String,Object>> entry : status.entrySet()){
assertEquals(entry.getValue().get(BALLOTS_REMAINING).toString(), "0");
assertEquals(entry.getValue().get(ESTIMATED_BALLOTS).toString(), "0");
assertEquals(entry.getValue().get(DISCREPANCY_COUNT).toString(), "{}");
}

LOGGER.debug("Successfully completed Demo1.");
LOGGER.info("Successfully completed Demo1 (First round audit; No Discrepancies).");
}

}
Loading
Loading