Skip to content

Cross-checking our IRV cases against rcv-lab.org

Level: reference · deep dive

One line: every ranked RCV-IRV case in this library was re-counted by an outside engine — rcv-lab.org — and 63 of 64 agreed; the one that didn't turned out to be a hidden tie in one of our own cases, which is exactly what a cross-check is for.

→ the platform: RCV Lab · sibling referees: pref_voting · BetterVoting · RCTab · tool: build_rcvlab_sankey.py.


Why bother

The Ranked Robin cases get counted three ways — this engine, BetterVoting, and pref_voting's independent Copeland — because a tally that only ever agrees with itself is not evidence. The RCV-IRV cases had no such leg: the vendored pyrankvote engine was the only thing counting them. This closes that gap.

What was run

All 64 ranked cases with an IRV method, converted to RCV Lab's generic CVR CSV and re-counted by its own engine. Nothing was sampled and nothing was skipped.

Cases counted 64
Winner agrees with our answer key 63
Disagrees 1 (a tie — see below)
Sankey diagrams exported 26

The one disagreement, and it was ours

coombs_ex20_district1 — we say B, RCV Lab says C. Neither engine is wrong. Round 1 is C 16, A 9, B 9, and that tie decides the election: eliminate A and its A>B>C ballots elect B 18–16; eliminate B and its B>C>A ballots elect C 25–9. Our engine drops A, RCV Lab drops B, and neither prints that it broke a tie.

The case's own description had asserted "IRV elects B here too" as a plain fact. It is a fact about our tiebreak, not about IRV, and the file now says so. The reinforcement paradox the case exists to demonstrate is untouched — District II and the amalgamated file have no tie — but the claim needed narrowing, and only a second engine was ever going to catch it.

The general lesson: a silent tiebreak reads exactly like a determinate result. It is worth re-reading any teaching page that leans on an IRV elimination order in a small hand-built electorate.

One difference that is not a disagreement

RCV Lab reports round counts on a different convention: it eliminates candidates one at a time where our engine clears a whole batch that cannot catch the next candidate up. On cycle_vote_on_the_rule_irv_c5_b999 it says four rounds and we say three, with every tally identical. Round count is a reporting convention, not a result — don't cite "it took N rounds" as a fact about an election without saying whose count.

The Sankey diagrams

The 26 cases that run to three or more rounds now carry a committed img/<stem>_sankey.svg beside the case, drawn by RCV Lab and exported as a self-contained SVG.

They earn their place because our engine prints rounds as a table, and a table cannot show where a transferred vote came from — which is the whole subject of every page here that argues about center squeeze or exhausted ballots. RCV Lab traces exact ballot provenance rather than estimating the ribbons, and it gives exhausted ballots their own column, labelled "No Further Rankings".

They are committed as static files, not linked: the repo does not control that site, it is openly a beta whose output may change, and a stored SVG renders on GitHub, on the built site, and in a local viewer with no network at all.

Reproducing it

python3 STARVote_LH_tabulation_engine/tools_adam/scripts/build_rcvlab_sankey.py emit --out /tmp/rcvlab

That writes one CVR CSV per case. Upload each at rcv-lab.org → Analysis → Ballot data (CVRs) → Tabulate → Visualize → Sankey, collect {stem, winners, rounds, svg} into a JSON array, then:

python3 STARVote_LH_tabulation_engine/tools_adam/scripts/build_rcvlab_sankey.py install results.json

which re-checks every winner against the YAML answer key and writes the art. Three things about that loop are worth knowing before repeating it, all of them silent failures:

  1. Their generic CSV eats the first column as a ballot ID. The docs say "one column per candidate" and mean starting at column two. A bare candidate header loses the first candidate and returns a confident wrong winner — our Post-it case came back "Green in 2 rounds" instead of Purple in 3. emit always writes a leading BallotID column; the site then reports "In CVR but not in config: BallotID", which is the sign it parsed correctly.
  2. The filename must contain cvr, or the site raises a confirm() that an automated browser suppresses, silently rejecting the file.
  3. The derived config is computed once and never refreshed. Load a second CVR without pressing Reset and it is scored against the first file's candidate list, silently producing an empty 0-round election.

Scope

Ranked ballots only. RCV Lab counts IRV, STV and bloc, plus plurality/approval/cumulative for comparison — no STAR, no Ranked Robin. It can never check the methods this library is mostly about; for those, pref_voting and BetterVoting remain the referees.

The full ledger

Counted 2026-08-08. ✅ = winner matches our expected_winners.

Case Rounds Ours RCV Lab Art
ballot_expressiveness_c9_irv_top5 8 Ben Ben
bv2280_37yf8x_irv_full 8 Ben Ben Sankey
crowded_field_c7_irv 6 Clara Clara Sankey
burlington_2009_irv 5 Kiss Kiss Sankey
burlington_2009_raise_kiss_nonmono 5 Montroll Montroll Sankey
street_trees_five_rounds_c6_b100 5 Birch Birch Sankey
bv2138_cxrf8v_irv 4 Dave Dave Sankey
bv2183_dfw8rj_forced_exhaustion_ceiling 4 Ada Ada Sankey
bv2278_8cdkkc_five_way_irv 4 A A Sankey
bv2281_qycpbx_ossipoff_irv 4 D D Sankey
crowded_field_c5_irv 4 Elsa Elsa Sankey
cycle_vote_on_the_rule_irv_c5_b999 4 Ranked Pairs Ranked Pairs Sankey
batch_all_out_round2_c4_b6 3 Alex Alex Sankey
bv2133_dyxrbr_pet2_irv 3 Fish Fish Sankey
bv2162_4htk44_irv 3 A A Sankey
bv2163_74j6vv_irv 3 B B Sankey
bv2170_pp2q4q_irv 3 Avery Avery Sankey
bv2176_p8dp28_irv 3 Purple Purple Sankey
bv2178_8kg698_irv 3 Blue Blue Sankey
bv2277_tqfdbg_mayor_irv 3 Cora Cora Sankey
bv2282_hf3ckp_brams_irv 3 B B Sankey
coombs_ex18_monotonicity 3 Arlo Arlo Sankey
coombs_ex21_twin_after 3 B B Sankey
coombs_ex21_twin_before 3 B B Sankey
coombs_ex22_scc 3 A A Sankey
irv_combined 3 A A Sankey
put_two_universes_c3_b4 3 Anna Anna Sankey
alaska_buried_c3_b200 2 Peltola Peltola
alaska_sincere_c3_b200 2 Peltola Peltola
alaska_upward_after 2 Begich Begich
alaska_upward_before 2 Peltola Peltola
balance_base_irv_c3_b9 2 Bruno Bruno
balance_plus_opposite_c3_b15 2 Ada Ada
batch_all_out_condorcet_c3_b3 2 Amy Amy
batch_all_out_cycle_c3_b3 2 Amy Amy
bv2132_ykjjhy_pet_irv 2 Fish Fish
bv2137_ywckmg_irv 2 Carter Carter
bv2145_6fj2kg_irv 2 Ben Ben
bv2146_krk2px_irv 2 Ada Ada
bv2147_9gdrqg_irv 2 Bruno Bruno
bv2149_byk9v2_irv 2 Alma Alma
bv2150_dxg8pb_irv 2 Carl Carl
bv2151_97hbpw_irv 2 Beth Beth
bv2154_wq6yv7_irv 2 Clara Clara
bv2222_rfyk46_510_thin_irv 2 Ana Ana
bv2223_dyh93j_510_real_irv 2 Ana Ana
bv2227_3xgkck_honest_irv 2 Right Right
bv2228_bgcmxx_betray_irv 2 Center Center
condorcet_1788_irv 2 Peter Peter
coombs_ex20_amalgamated 2 B B
coombs_ex20_district1 2 B C ⚠️
irv_district_A 2 B B
irv_district_B 2 B B
margins_irv 2 Cocoa Cocoa
omr_opposition_decides 2 B B
reversal_irv_original 2 A A
reversal_irv_reversed 2 A A
sf_d7_downward_after 2 Engardio Engardio
sf_d7_downward_before 2 Melgar Melgar
tilted_cycle_c3_b5_irv 2 Cara Cara
bv2148_h87k6v_irv 1 Bruno Bruno
bv2153_pcttmr_irv 1 Amos Amos
coombs_ex20_district2 1 B B
crowded_field_c3_irv 1 Diego Diego

Up: tabulation engines · 07_Concepts · related: RCV Lab · RCTab · cross-checking with pref_voting.