starvote_larry_hastings.py — presentation wrapper¶
A thin presentation/automation layer over the vendored starvote engine (see FORK_NOTES.md). The engine does the tabulation; this script handles how elections are loaded, run, displayed, and saved. It never duplicates engine logic — it import starvote and feeds it ballots.
Use it to run a single election file; every run also refreshes that file's committed _tabulated mirror.
Quick start¶
# Run one election file (YAML or starvote CSV), from the repo root
uv run python STARVote_LH_tabulation_engine/starvote_larry_hastings.py \
01_STAR/02_Examples/cases/09_c4_b100_tennessee-capital.yaml
Color is shown automatically in a real terminal and in PyCharm's run console. Set NO_COLOR=1 to force plain output anywhere.
Election file format¶
A .yaml/.yml file supplies the ballots plus a few optional fields. The loader tolerates flat files, a single election: wrapper, or a races: list.
election_title: Tennessee Capital — classic STAR example
scenario_description: |-
Free-text context, printed as a header above the results.
num_winners: 1 # -> seats
voting_method: STAR # see "Methods" below
ballots: |-
Memphis,Nashville,Chattanooga,Knoxville
42: 5, 4, 3, 2 # "weight: scores" — 42 identical ballots
26: 2, 5, 4, 3
election_titleprints as a one-line banner;scenario_description(also accepted asrace_description/election_description) stays in the file and the_tabulatedcopy — it reaches the screen only via--fullorshow_description: true.- Ballots may use
weight: scores(e.g.42: 5,4,3,2) to stand in for many identical ballots. Empty cells and marker characters count as score 0.
Methods (voting_method)¶
| Value | Method | Winners |
|---|---|---|
STAR / star |
STAR Voting | single (num_winners: 1) |
bloc / bloc star |
Bloc STAR | multi (num_winners: >= 2) |
allocated |
Allocated Score Voting (proportional STAR) | multi |
sss |
Sequentially Spent Score | multi |
rrv |
Reweighted Range Voting | multi |
A method/seat mismatch (single-winner method with num_winners > 1, or a multi-winner method with num_winners: 1) is rejected with an explanatory error.
Display options¶
Set under options: (top level or per race). All are booleans unless noted. The defaults are the house on-screen style, so case files normally carry no options: block at all; set a key only to override a default, or pass --full on the command line to put the mirror's everything-on render on screen for one run.
| Option | Default | Effect |
|---|---|---|
show_matrix |
on | Show the Runoff (Preference) Matrix — the head-to-head / pairwise grid. Auto-suppressed for multi-winner and two-candidate races. When a multi-winner race does render it (mirror, --full, or a file override) the grid prints unmarked as a plain "Preference Matrix" — the Top-2 finalist markers are a single-winner concept. |
matrix_finalists_only |
on | Restrict the matrix to just the two finalists (the decisive runoff matchup). Requires show_matrix. |
show_condorcet |
off | Show the [Condorcet Winner] line. |
show_score_counts |
off | Show the per-candidate [Score Distribution] table. |
brief |
on | Collapse repetitive [STAR Voting: …] section headers into plain sub-headings. |
collapse_ballots |
on | On-screen report identical ballots as count × scores (most common first) instead of one row each. |
count_separator |
× |
Separator for the collapsed count: ×, :, or x/X (all round-trip to valid input). |
Output conventions¶
- Equal Support is the relabeled "no preference" bucket in the runoff, shown as
Equal Support -- N(plain — the aka is documented once in GLOSSARY.md). ("Equal Support" is the Equal Vote Coalition's term.) - Runoff colors mirror the matrix legend: the winner's count is green (For), the other finalist's is red (Against), and Equal Support is blue (Equal Support). A tie is left neutral until it resolves; tiebreaker rounds (raw scores) are uncolored.
- Round separators: in multi-round methods (e.g. Bloc STAR) a faint rule is drawn before each new round after the first, grouping the output into blocks.
- Winner line restates the method and winner count, e.g.
Winner (STAR Voting Method — single winner)orWinners (Bloc STAR Method — 3 winners). - Setup line (multiwinner): the base engine's standalone
Want to fill N seats.is folded into the ballot-count line, so the two "size of this election" facts sit together —Tabulating 4 ballots to fill 3 seats.Single-winner output is unchanged. (Done in the wrapper'scustom_print; the vendored engine is untouched.) [Score Distribution](withshow_score_counts): a per-candidate star histogram, captioned(how many ballots gave each star rating), with aScorecorner label and a===rule row so the5 4 3 2 1 0header reads unmistakably as star values. The Avg column is computed from an exact rational and rounded half-up to one decimal (not float/+{:.1f}, which rounds half-to-even and would print an exact1.25as1.2). See Score Distribution & averages.
Change log — wrapper display (this is our code, not the vendored engine)¶
For the full consolidated list of all LH changes (methods, reports, fixes across the whole engine, not just display), see
LH_ENGINE_CHANGES.md. The entries below are the wrapper's display-layer edits specifically.
Behavioral edits to starvote_larry_hastings.py's presentation layer. The vendored starvote/ core stays pristine (see FORK_NOTES.md); these never touch it.
- House defaults built in (2026-08-09) —
DEFAULT_OPTIONSis now the house on-screen style:show_matrix+matrix_finalists_only+show_runoff_percenton by default (the matrix auto-suppressed for multi-winner and two-candidate races); case files carry nooptions:block (501 removed repo-wide; an override remains legal); new--fullflag renders the mirror's everything-on report on screen;show_irvis accepted for parse-compat only (the divergence block always prints). - Ranked Robin equal-rankings (
A=B>C) — parser fix —run_ranked_robin's ranked-ballot reader now splits each>rank level on=, so tied candidates share a rank and are scored as Equal Support against each other (exactly how Ranked Robin treats a tie). Previously the parser split only on>, so a level likeAva=Bianca=Cedricwas mis-read as a single phantom candidate by that literal name — inflating the field and electing the wrong winner. Strict ballots (every level a singleton) are byte-for-byte unchanged. Equal ranking is a core Ranked Robin feature, so this lets the engine read the weak orders RR is defined on natively (e.g. the electowiki worked example). Guarded bytests/test_ranked_robin.py::test_equal_rankings_are_ties. [Score Distribution]header + exact-rational half-up average —Scorecorner label,===rule row, star-rating caption, and the Avg float→Decimal/ROUND_HALF_UPfix. (full write-up)- Multiwinner setup line — merged
Want to fill N seats.intoTabulating N ballots to fill N seats. - Earlier:
[Lot-decided tie — rare]callout; validator acceptsvoting_method: Bloc STAR.
Saved _tabulated files¶
Every run of a file also writes a plain-text copy into a sibling mirror folder whose name is the source folder + _tabulated, with the file itself also suffixed _tabulated:
03_STAR_PR/02_Examples/cases/foo.yaml
-> 03_STAR_PR/02_Examples/cases/cases_tabulated/foo_tabulated.txt
Each _tabulated.txt contains:
- A header recording the source name and the tabulated name:
======================================================================
SOURCE FILE: bar.yaml
TABULATED FILE: bar_tabulated.txt
======================================================================
(No timestamps, deliberately: the mirrors are committed to git, so regenerating them on any machine yields a byte-identical file whenever the tabulation content is unchanged. Git history is the record of when a mirror last changed.)
2. The original election file, copied verbatim.
3. A TABULATION RESULTS section.
Important: the saved file is always rendered with the full, most explanatory output (every analysis on, full N×N matrix, non-brief headers), regardless of the file's own options:. Only the on-screen output honors the file's options. This keeps the on-screen demo clean while the saved record is complete. (The console no longer prints a "tabulated copy" path, to avoid distracting an audience.)
Add --save to additionally embed an expected_results: block (winners + plain-text report) back into the source YAML.
Keeping mirrors fresh¶
There is deliberately no wipe-everything batch runner anymore (the old
tabulate_all.py predates the per-cases/ mirror layout and was retired
2026-08). Re-run the file(s) you changed — each run rewrites its own mirror —
and tests/test_tabulated_mirrors_current.py fails naming any mirror whose
source YAML changed without a re-run. Pages/index regeneration:
tools_adam/scripts/regen_all.py.
Example library¶
The example elections live in the repo's method folders (01_STAR/ …
06_Other/, method_comparisons/), each under a cases/ directory with its
cases_tabulated/ mirrors and generated cases_pages/. Browse them all by
method in the YAML test-case index.