The LH starvote engine — what it is, and what we added¶
The repo's primary tabulator. It's Larry Hastings' STAR engine plus a thick presentation + analysis layer of our own — the part that turns a correct winner into a readable, auditable report. This page is the map; the section-by-section walkthrough is Reading a STAR report.
Looking for the exhaustive change list? This page is the narrative overview; the itemized, canonical changelog of every method / report / fix is
LH_ENGINE_CHANGES.md.
→ Cross-checks: pref_voting (independent referee) · BetterVoting and the engine. Source: larryhastings/starvote · PyPI. Curriculum: 201.1.
Credit — starvote is Larry Hastings' engine¶
The tabulation core is starvote, written and maintained by Larry Hastings (PyPI, MIT-licensed). It's a mature, well-tested Python implementation of STAR Voting and the whole family of related score methods — single-winner STAR, multi-winner Bloc STAR, and three proportional methods (Allocated Score, Sequentially Spent Score, Reweighted Range Voting) — with a pluggable tiebreaker system, a CLI, and its own .starvote/CSV file format. All the correctness of our results rests on Larry's engine; everything this repo adds sits on top of it, never replacing its math. When this page says "the LH engine," it means Larry Hastings' starvote with our presentation layer wrapped around it — full credit for the tabulation belongs upstream.
We use it as a vendored fork (a copy committed into this repo) only so the teaching examples are reproducible against a known version. We don't change the method — the handful of edits we do carry are two output toggles and three upstream bug fixes, all itemized below and offered back to Larry. See Fork Notes — starvote (vendored fork) for the exact upstream baseline (tag starvote-upstream-2.1.6) and how to re-pull a future release.
Two layers (so you know what's "ours")¶
The engine is a vendored fork of upstream starvote 2.1.6, and almost everything we describe as "our improvement" lives in the wrapper, not the algorithm:
| Layer | File | What it owns |
|---|---|---|
| Upstream engine | starvote/ |
The voting algorithm — scoring, runoff, tiebreak mechanics. We touch it almost never. |
| Our wrapper | starvote_larry_hastings.py |
How elections are loaded, run, displayed, and saved — the reporting, the matrix, the colors, multi-method dispatch, error handling. |
So when we say the LH engine "reports better than upstream," we almost always mean the wrapper (starvote_larry_hastings.py — presentation wrapper) — not edits to Larry's code.
Exactly what we changed in the vendored engine¶
The vendored starvote/ package is kept as close to pristine as possible. Compared to upstream starvote 2.1.6 (git diff against tag starvote-upstream-2.1.6):
- Method and count: unchanged. Same
__version__(2.1.6), no functions added or removed except one small helper, and the two files are ~97 % character-identical once whitespace is ignored. The large raw line count ingit diff --statis almost all line-reflow (long signatures/calls re-wrapped), not logic. Nothing below is a rule change: we have not altered how a ballot is scored, how finalists are chosen, or how the runoff is decided. - Two new optional output toggles:
print_averages— defaultFalse; CLI-a/--print-averages; config keyprint averages. Suppresses the per-candidate averages line unless asked.print_maximum_score— defaultFalse; CLI-M/--print-maximum-score; config keyprint maximum score. Suppresses the "Maximum score is …" line.- a
bool_converterhelper to parse those two config keys. Both options are only forwarded to method functions when they differ from the default, so reference method implementations don't break. - Three upstream bug fixes — each one restores the behaviour the code already intended, and each is itemized with its repro and regression guard in
FORK_NOTES.md: - SSS ballot allocation gated on verbosity (2026-08) — the one that did change winners: at the default
verbosity=0Sequentially Spent Score never spent or reweighted ballots, quietly degenerating into repeated bloc score voting. Reported upstream as issue #17 (open). - Five-star tiebreak default score (2025) — a
.get()default of1instead of0for the second candidate in the two-candidate fast path; latent, and dormant on a 0–5 scale. - Unbreakable-tie message placeholder (2026-08) — two missing
fprefixes in_star_round(), soUnbreakableTieErrorechoed its own source text instead of "three-way tie in Scoring Round". Cosmetic, API-only, and no winner changes; see the errata note. Reported upstream as issue #18 (open).
What is not an engine edit (a common misconception — these are all in the wrapper): the No Preference → Equal Support relabel, the Runoff (Preference) Matrix, [Divergence from STAR], the [Runoff Reversal] summary, and show_runoff_percent. The vendored package still prints "No Preference" internally; our wrapper renders "Equal Support" on screen. Keeping the engine pristine but for these few documented edits is deliberate — re-pulling a future upstream release stays trivial.
To reproduce this list precisely at any time:
git diff --stat starvote-upstream-2.1.6 -- STARVote_LH_tabulation_engine/starvote/
git diff starvote-upstream-2.1.6 -- STARVote_LH_tabulation_engine/starvote/
The headline design: on-screen report vs _tabulated¶
The single most important improvement is that every run produces two outputs, tuned for two different readers:
1. The on-screen report — minimal by design. The engine's built-in defaults are the house less-is-more style: the rounds, the finalists matrix, the self-reconciling runoff line, the winner. Clean enough to put on a slide without overwhelming a first-time voter. (A file may still set options: to override any default, and the --full flag puts the mirror's everything-on render on screen for one run.)
2. The saved _tabulated.txt mirror — always maximal. Every run also writes a plain-text copy into a sibling <folder>_tabulated/ mirror, and that copy is always rendered with every analysis on — full N×N matrix, Condorcet line, score distribution, tiebreaker detail, the runoff funnel — regardless of the file's options. It also stamps the source name, the source's last-modified time, and when tabulation ran, so a stale record is obvious at a glance.
The payoff: the on-screen demo stays clean while the saved record stays complete. You never have to choose between a readable on-screen report and a full audit trail — you get the right one in each place. (This is why house YAML files carry no options: block at all: the engine's defaults already render the house style, and the mirror forces full detail anyway, so hand-setting options just adds noise.)
What the report adds on top of "the winner is X"¶
Beyond the raw result, the wrapper renders the parts that make a STAR result trustworthy and teachable:
- Runoff (Preference) Matrix — the head-to-head / pairwise grid, the summable heart of the count (
For – Equal Support – Againstper cell). See STAR is summable. [Runoff Reversal]summary — a plain-English sentence naming why the runoff winner beat the score leader when they differ: the runoff elects the finalist preferred by the majority (of voters with a preference). The whole Runoff-Reversal lesson in two lines. (Header formerly "Majority Preference Enforcement Principle" — renamed to the glossary term.)[Divergence from STAR]— a quick cross-check that flags when Approval / a pure score count, or RCV-IRV, would pick someone else; it prints whenever methods disagree, so comparative demos keep their punch even with the minimal on-screen report.- Self-reconciling runoff line (
show_runoff_percent) — a compact two-line summary of the winner's share of the decided voters, stated against the total ballots with the Equal Support gap named inline so the denominator never has to be inferred; the_tabulatedcopy expands it into a "Runoff math" funnel. Details: runoff percentages. - Score distribution, Condorcet line, tiebreaker rounds — on demand on screen, always in the mirror. The lot-number tiebreak cascade (head-to-head → most 5s → lot order) is shown step by step so a tie's resolution is auditable.
- Color palette + round separators — distinct colors per phase (scoring, runoff, winner), mirroring the matrix legend; auto-off when piped to a file or under
NO_COLOR, so the saved text stays clean.
One engine, many methods (auto-dispatch)¶
The wrapper reads voting_method and routes to the right tabulator, so the same command and the same _tabulated discipline cover the whole family:
- STAR (single), Bloc STAR, and proportional Allocated / SSS / RRV (multi).
- RCV-IRV (Hare) — ranked ballots (
A>B>C) route to the vendored RCV-IRV engine. - Approval — single or explicit multi-winner.
- Ranked Robin (RCV-RR / Copeland) — first-class:
voting_method: RankedRobin(aliasesRCV_RR/Copeland/Consensus) prints the round-robin report (ballots + pairwise table + win-loss record), flags a Condorcet cycle, and writes its own_tabulatedmirror — it does not fall through to the IRV rounds. See Ranked Robin.
It also runs an extra blocs: vote-splitting check and supports quorum / eligible-voter accounting on STAR races.
Input ergonomics & clear errors¶
- Tolerant loader: flat files, a single
election:wrapper, or araces:list; weighted ballots (42 × 5,4,3,2), and zero-scoring markers (-blank,~race abstention,&candidate abstention,?spoiled,%reissued). - It errors clearly — no tracebacks — for the common mistakes: bad YAML, a missing
ballots:block (it prints the template), wrong column counts, out-of-range scores, ranked ballots under a score method, and method/seats mismatches. A missingvoting_method/num_winnersis a non-fatal NOTE.
Trust, but verify¶
Correct and readable isn't the same as self-certified. The repo's third engine wraps Eric Pacuit's pref_voting as an independent referee and confirms the LH engine's Condorcet / IRV / Plurality / Copeland picks across the whole library (currently 0 mismatches): cross-checking with pref_voting.
Related pages¶
- Reading a STAR report — the section-by-section walkthrough.
- The
.starvoteballot file format — Larry's own INI-like input format ([options]+[ballots]), the three front doors and their differing tiebreak defaults, and a worked Bloc STAR race that ties at every rung. - BetterVoting and the LH engine — the visual half of the same race.
- Runoff Reversal — the phenomenon the report is built to expose.
- starvote_larry_hastings.py — presentation wrapper · Fork Notes — starvote (vendored fork) — the engine docs themselves.