A voting simulation harness — goals, requirements, and the existing landscape¶
Level: reference · deep dive
One line: for the six methods this library teaches, no existing simulator covers more than half of them — the closest is VMES in Julia, not vse-sim in Python — and the case for writing the harness in Rust turns out to depend entirely on which of two workloads you want, with a measured 2.9× gap for one and a measured 238× gap for the other.
This is a decision document, not a plan of record. Nothing here has been built. It answers the prior question — what would a simulator be for, and what would it have to do? — before any language argument, and it puts the landscape survey in front of the requirements so the requirements can be harvested from what other people already learned rather than invented.
Companions. A Rust voting kernel — goals and requirements and A Rust tabulation kernel — scope work the same question for the counter. Their answer was no (2026-08-10), and that answer does not transfer here: a test library's dominant cost is authoring, a simulator's is running. That is a different calculation, and Part 6 does it with a benchmark instead of an intuition.
Read first if you are starting cold: Election simulation models (the menu of electorates) · Simulate utilities, not ballots (why utilities are the primitive) · vse-sim, read from source (the canonical simulator, in detail) · this folder's README (what already runs here).
What this library already has¶
Not nothing — which changes the question from "should we build a simulator?" to "what would a harness add to twelve scripts that already work?"
| state | |
|---|---|
| simulation scripts in this folder | 12, each answering one question, each uv run-able standalone |
| voter models available | six Euclidean spaces + the statistical cultures, both implemented and cross-checked against prefsampling |
| single-winner methods reimplemented in numpy | 6 — STAR, Score, Approval, Ranked Robin, RCV-IRV, Plurality (condorcet_efficiency_simulation.py) |
| multi-winner methods simulated | 3 — Allocated Score, SSS, RRV — but only inside pr_alabama_paradox.py, and only via starvote at ~350 elections/s |
| Bloc STAR simulated | none |
| strategy modelled | two scripts — fbc_simulation.py, strategic_cw_preservation.py |
| VSE computed | as a side metric in two scripts; there is no cross-method VSE table for this library's own six methods |
So the gaps are specific rather than general: multi-winner, Bloc STAR, and a shared VSE surface. Everything else exists in some script, in a form that cannot be reused by the next script because each one reimplements the methods it needs.
That duplication is the real argument for a harness, and it is worth naming precisely. Twelve scripts share sample_utilities() by copying it or by importing across files (score_encoding_stability.py imports from star_vs_rr_divergence.py, which imports from condorcet_efficiency_simulation.py). That chain works, and it is one edit away from a silent divergence between two scripts that both claim to model the same electorate.
Part 1 — What a simulation would be for¶
The discipline from the kernel requirements page applies unchanged: each goal below produces a materially different program. Pick one. The rest are allowed to happen for free or not at all.
S1 — Reproduce and audit the published VSE numbers¶
Success looks like: this repo can state "we ran it; here is the number and here is the config" for the "STAR scores 91%" charts, instead of citing them.
Demands: exactly vse-sim's electorate (KSModel, 40 voters, 6 candidates), exactly its strategy models, and exactly its metric — because a reproduction that changes the model is not a reproduction.
Verdict: already 90% free, and the remaining 10% is not a simulator. The published config is a single documented command, the code is public, and this repo has read it from source and written down every setting. The one obstacle is that the reproduction script does not currently run — a two-line import fix, verified end-to-end. Fix that upstream and S1 is done without writing a simulator. Do this first regardless of everything else on this page.
S2 — Measure this library's own claims, on one shared electorate¶
Success looks like: one table, six methods, per electorate model, with VSE and Condorcet efficiency and divergence rates, all computed from the same sampled utilities in the same run — so the rows are comparable to each other and not just to themselves.
Demands: the six methods on one shared preference primitive, a fixed set of electorate models, and a self-describing output format. Nothing exotic.
Verdict: the strongest goal, because it is the one that serves the readers. Every other item on this list benefits the maintainer. This one puts a number under sentences the library currently has to hedge — and it is mostly consolidation of code that already exists, which makes it the cheapest real goal too.
S3 — Strategy and manipulability¶
Success looks like: a PVSI-style number per method, with the sincere baseline beside it, and the coalition size a successful attack needed.
Demands: far more than S2 — a strategy model per method per attack type, a polling/information model, and a chooser that decides who defects. This is where simulators get big: vse-sim carries three ballots per voter per method for exactly this reason.
Verdict: high value and high cost, and the place where an existing library genuinely earns its keep. VMES already implements it for these methods; writing a third one is hard to justify unless the goal is to disagree with it.
S4 — Multi-winner: Bloc STAR and STAR-PR¶
Success looks like: a defensible answer to "how proportional is Allocated Score, actually?" and "how much does Bloc STAR over-represent the majority?" — measured, not asserted.
Demands: the hard part is not the tabulation, which starvote already does. It is that the metric is unsettled. Single-winner welfare has an agreed shape (VSE against the best single candidate). Multi-winner does not: you must choose between utilitarian welfare over the committee, a proportionality axiom (JR / PJR / EJR / FJR, checkable by abcvoting, already a dependency here), Skowron's proportionality degree, or coverage — and these conflict with each other by construction, so the choice decides the ranking before a single election is run.
Verdict: the largest genuine gap, and the one nobody else has closed for STAR-PR specifically. It is also the goal most likely to produce something publishable in the research-topics companion repo, precisely because the metric question is open rather than settled.
S5 — Exhaustive search and the counterexample factory¶
Success looks like: "no 3-candidate, 7-ballot STAR election exhibits a monotonicity failure" — proved by enumeration, with a witness emitted as a YAML case file when one exists.
Demands: raw speed, an allocation-free kernel, and symmetry reduction. See Part 6 — this is the only goal on the list where the language choice is load-bearing, and even there it buys less than the ratio suggests.
Verdict: the most interesting goal, and the one where a result is a finding rather than a chart. Also the one that most needs sizing before it is promised: the frontier is sharp and it is closer than it looks.
S6 — Interactive simulation on the teaching site¶
Success looks like: a reader drags a candidate across an issue space on a published page and watches the STAR winner change — a live Yee diagram.
Verdict: genuinely valuable teaching, and it is the same capability question as G1 on the kernel page, with the same amendment: price Pyodide before assuming a second language is required. A Yee diagram is a few thousand tiny elections per frame — which is the fast shape, so this and S5 want the same kernel.
S7 — Learning Rust on a domain with known answers¶
Verdict: entirely legitimate and worth saying out loud if it is the real driver, exactly as G5 has it. If it is the goal, scope it to one method and one model, put it in the Rust learning library rather than here, and let this page's requirements be aspiration rather than specification.
Part 2 — The pipeline every simulator has¶
Every serious simulator surveyed below has the same four-stage shape, and the clearest statement of it is 19 years old — Warren Smith's IEVS, in a comment at the top of a 5,905-line C file:
Software Architecture: I. voting methods. II. voting strategies. III. ignorance generators. IV. utility generators. […] The information-flow-direction is IV→III→II→I→winners→regrets. […] The idea is to build a "Chinese menu" system which can investigate A*B*C*D kinds of scenarios BUT the effort to write it is only A+B+C+D.
That is the single most important design requirement on this page, and it is the one a script-per-question folder violates by construction. Twelve scripts each hard-wire one path through the menu; a harness makes the four stages independent so that adding a method costs one implementation rather than twelve.
| Stage | What it produces | Where the modelling choices hide |
|---|---|---|
| IV. Utilities | a real number per (voter, candidate) | the electorate model — spatial vs statistical, dimensions, clustering, candidate placement |
| III. Information | each voter's perceived utilities and the public polls | how wrong voters are about their own interests, and how wrong strategists are about the standings |
| II. Ballots | one ballot per voter per method | the encoding rule (min-max? global scale?), the approval cutoff, truncation, and any strategy |
| I. Tabulation | winners, plus the audit trail | tie ladders — the one place two correct implementations legitimately disagree (the ladders, written down) |
| Metric | VSE, Condorcet efficiency, divergence, proportionality | the multi-winner problem from S4, and whether a sincere baseline is printed |
Stage III is the one most often collapsed into stage II, and it should not be. IEVS keeps them separate — AddIgnorance() perturbs each voter's true utility by Gaussian noise before any ballot is written, with two modes: a constant amplitude for everyone, or a stratified one where different voters are differently well-informed. vse-sim instead models ignorance only at the poll level (fuzzyMediaFor), so its voters know their own preferences perfectly and are merely wrong about who is winning. These are different claims about people, and a chart built on one of them cannot be compared to a chart built on the other. A harness should carry both and say which was used.
Part 3 — Requirements¶
Written as if S2 were the primary goal, with notes where another goal changes the answer.
Non-goals — state these first, they do more work¶
- NG-1. The harness does not replace the twelve scripts. They are each a finished answer to a question and several are cited by teaching pages; a harness that requires rewriting them has negative value on day one.
- NG-2. The harness does not tabulate through the LH engine's CLI. Measured below: process-per-election is ~5 elections/second against ~100,000 for an in-process kernel.
- NG-3. The harness does not draw random ballots. Ever. It samples utilities and derives ballots — the rung-2 rule, non-negotiable, and the single most common defect in amateur simulators.
- NG-4. The harness does not invent electorate models.
prefsamplingand the two reference scripts already define them, cross-checked; a third definition is a drift surface. - NG-5. The harness does not produce a number without the model beside it. See FR-9.
Functional requirements¶
| # | Requirement | Priority | Note |
|---|---|---|---|
| FR-1 | Tabulate the six methods on one shared electorate: STAR, Bloc STAR, STAR-PR (allocated / SSS / RRV), Ranked Robin, Approval, RCV-IRV | must | the point of the exercise |
| FR-2 | The preference primitive is utility; every ballot is derived, never drawn | must | NG-3 |
| FR-3 | The utility→ballot encoding rule is a parameter, not a constant | must | measured to move the STAR winner 6.8%–27.2% depending on the space |
| FR-4 | The Approval cutoff is swept, not fixed | must | Approval has no canonical sincere conversion; the divergence rate moves 10%→40% non-monotonically with it |
| FR-5 | Ties are reported as ties, and any tiebreak used is named in the output | must | "never let an arbitrary tiebreaker silently inflate a result" — this folder's own standing rule |
| FR-6 | Every strategic number is printed beside its sincere baseline | must | the merged-number failure, which makes Plurality tie Ranked Robin |
| FR-7 | Output is self-describing: model, parameters, seed, method set, election count, and the code version, in the file | must | vse-sim's CSV header does this and it is the best thing about it |
| FR-8 | Report a confidence interval, not just a mean | must | the omission in almost every published VSE chart; the per-election rows make it free |
| FR-9 | Voter-ignorance and polling are separate, independently settable stages | should | Part 2 |
| FR-10 | A multi-winner run states which proportionality/welfare metric it used | must (S4) | they conflict; the choice is the experiment |
| FR-11 | Emit a discovered counterexample as a valid YAML case file | must (S5) | a finding that does not land in the case library is a finding that evaporates |
| FR-12 | Cross-check every method against an independent implementation on a sample of runs | should | the house method; pref_voting, abcvoting, starvote |
Non-functional requirements¶
| # | Requirement | Rationale |
|---|---|---|
| NFR-1 | Deterministic given a seed, and the seed is in the output | a simulation that cannot be re-run is not evidence |
| NFR-2 | Seeding is verified, not assumed | prefsampling 0.1.24 silently degenerates when seeded — filed, and it is this repo's own bug report, so the failure mode is not hypothetical |
| NFR-3 | Arithmetic policy for the PR family is stated and uniform | see Part 5's arithmetic lesson — floats make the result depend on ballot order |
| NFR-4 | A control method is present in every comparison | Ranked Robin must read 100.0% Condorcet efficiency; any other value means the harness is broken, and the existing script prints it for exactly that reason |
| NFR-5 | Any bound on coverage — top-N, sampled encodings, truncated sweeps — is logged, not silent | silent truncation reads as "we covered everything" |
| NFR-6 | Reproduction commands are executed by a test | the vse-sim reproduction gap: a documented command no test runs is not a command |
What each of the six methods specifically demands¶
| Method | Ballot from utilities | The awkward part |
|---|---|---|
| STAR | per-voter min-max onto 0–5 | the encoding rule (FR-3); the runoff tie ladder |
| Bloc STAR | same ballot, N seats, no reweighting | the metric — a majority sweep is the expected behaviour, so "it over-represents" needs a number that says by how much against what baseline |
| STAR-PR | same ballot; quota or divisor reweighting | fractional weights → NFR-3; and the count-vs-weight bug is a live example of a plausible wrong number surviving in a published engine |
| Ranked Robin | rank by utility | cycles are the interesting case and the spatial models nearly never produce them — 0.15% vs 16.65% depending on the model, so a structured-domain-only sweep concludes cycle rules are interchangeable |
| Approval | cutoff — there is no canonical one | FR-4; the cutoff is the experiment, and IEVS (mean-based, coin toss at the mean) and vse-sim (midpoint of range) do not even agree on the default |
| RCV-IRV | rank by utility, optionally truncated | elimination order under a tie is a behaviour of a specific library, not a property of IRV; and truncation policy decides the exhausted-ballot rate, which is what half the teaching pages are about |
Part 4 — Is the Python VSE library useful?¶
Yes, as a specification. No, as a dependency. Those answers are cleanly separable and it is worth not blurring them.
As a specification it is close to indispensable, and this library has already extracted most of that value: the read-from-source page documents the exact VSE formula, the published run's electorate, what a "strategic voter" is under each chooser, what the strategy actually does per method family, and what the polling model is. Anything built here should adopt its two best habits outright — the self-describing CSV header (FR-7) and the always-computed sincere baseline (FR-6) — and should reuse its formula rather than re-derive one.
As a dependency it does not fit, and the reason is method coverage rather than code quality:
| Adam's six | in vse-sim? |
|---|---|
| STAR, single-winner | partly — as Srv(10), a 0–10 ballot. The 0–5 STAR ballot is Srv(5), which appears only in a secondary method set, not in the published run |
| Bloc STAR | no |
| STAR-PR (allocated / SSS / RRV) | no |
| Ranked Robin (Copeland) | no |
| Approval | partly — the published "Approval" line is Score(1), an ideal approver; the realistic variant is tracked separately as BulletyApproval60 |
| RCV-IRV | yes |
vse-sim is single-winner only. Its fourteen method modules — Borda, bullety approval, IRNR, IRV, IRV′, MAV, MJ, Plurality, ranked, Ranked Pairs, Schulze, Score, Srv, 3-2-1 — contain no multi-winner rule at all. So it covers one and a half of the six, and the two it half-covers are half-covered in the direction that matters: the ballot resolution is wrong for STAR, and the Approval line is a modelled ideal rather than a modelled voter.
Two further practical points. The licence is the one people get wrong: vse-sim ships a docs/LICENSE, but it is the Jekyll theme's MIT licence covering the website scaffolding — the simulator code itself carries no licence, which under default copyright means readable and citable, not vendorable. And the published-results script currently does not run at HEAD.
So the honest summary: read it, cite it, adopt its habits, fix its two-line bug upstream — and do not plan to import it.
Part 5 — The landscape¶
Fifteen projects worth knowing, grouped by what they are actually for. Star counts and dates are as of 2026-08-24.
Simulators (the direct comparables)¶
| Project | Language | Covers Adam's six? | What it is |
|---|---|---|---|
| VMES — Marcus Ogren | Julia | all six | The most complete match on this page. STARVoting(5) — the 0–5 ballot — plus RankedRobin, blockstar, allocatedscore, sss, seqmonroe, rrv, spav, mes, allocatedrankedrobin, sntv, rcv. Carries VSE and PVSI, a multi-winner mw_winner_quality, free-riding, and eight voter models. Active (last push 2026-08-20) |
| vse-sim — Jameson Quinn | Python | 1.5 of 6 | The canonical one; the published VSE charts are its output. Single-winner only. See Part 4 and the source read-through |
| elsim — "Election Simulator 3000" | Python | ~2 of 6 | MIT, actively pushed, clean three-stage API (elections → strategies → methods). Plurality, runoff, IRV, Hare, Borda, Coombs, Black, Approval, STAR. Documents its own throughput — ~25,000 elections/s with optional Numba. Also the source of the animated "core collapse" center-squeeze pictures |
| votesim / election_sim | Python | ~3 of 6 | MIT; scored methods, Condorcet (Smith-minimax, ranked pairs), IRV, and multi-winner reweighted range, Sequential Monroe, STV. Last push 2022 |
| IEVS — Warren D. Smith, 2007 | C | ~3 of 6 | 5,905 lines, 68 voting methods, 11 utility generators, capped at 32 candidates and 2,048 voters. Bayesian regret, Yee diagrams, and a "reality-based" utility generator built from Tideman's real-world election collection. The four-stage architecture in Part 2 is its comment block. Licence: non-commercial with acknowledgement — not open source |
| quadelect — Kristofer Munsterhjelm | C++ | ~2 of 6 | Not a VSE tool — a method-discovery tool, and the most methodologically interesting project here. Uses lil'UCB best-arm identification (src/bandit/) to spend simulation effort adaptively on the methods still in contention, and linear programming (glpk, src/linear_model/constraints/relative_criteria/) to solve for criterion counterexamples rather than sample for them. Has monotonator, find_distinguisher, test_vse_bandit, multiwinner_spatial. Active |
| kingmaker | Rust | ~1 of 6 | "A modular, performant, social choice framework for the simulation, computation, and analysis of strategic voting." Mallows preferences, tactics (burial, compromise) with probability weights, builder-pattern blocs. v0.1.0, ~600 downloads, last updated 2025-05 — the only Rust simulator found, and it is a seedling |
| elect | JavaScript | ~2 of 6 | Bayesian regret for multi-winner, via a genuinely different welfare model: voters are points on issues, candidates become "dictators" implementing their platform, and regret is measured over the resulting policies. Worth knowing because it is one answer to S4's open metric question |
| electionsim | Go | ~1 of 6 | Single-winner utility efficiency with random candidates and voters. Small, last push 2019 |
| votingMethods / voteSim | R | ~1 of 6 | An R package for testing methods on a generated simulation. Small |
Infrastructure this repo already uses (or should)¶
| Project | Language | What it supplies |
|---|---|---|
| prefsampling | Python | The profile and Euclidean samplers, already a dependency — with a seeding bug this repo filed that is the reason NFR-2 exists |
| pref_voting | Python | The independent cross-check — Copeland, Minimax, Coombs, the grade methods |
| abcvoting | Python | Multi-winner approval rules and the JR/PJR/EJR checks — which is the S4 metric machinery, already installed |
| mapof-elections (mapel) | Python | The academic framework: embeds whole elections as points in a 2-D similarity space so a dataset of electorates can be characterized rather than a single model asserted. Faliszewski, Szufa et al. |
What the STV implementations know that the simulators do not¶
Two Rust projects that are tabulators, not simulators — and both answer a requirements question the simulators leave open.
stv-rs implements Meek STV under five distinct arithmetic modes: fixed9, bigfixed9, float64, exact rationals, and approx. Its README states the two findings that matter: float64 makes the result depend on the order of the ballots, and exact makes "algorithm complexity explode" so it is impractical at scale. It also notes that parallelization requires associative and commutative arithmetic, which excludes float64 — so the arithmetic choice and the concurrency design are the same decision, not two.
ConcreteSTV reproduces the counts of actual Australian jurisdictions, including deliberately emulating the electoral commissions' known bugs, and has found and helped fix real errors in ACT, NSW and federal Senate counts. Its epistemics are the part to steal: matching an official result is, in its own words, "only very weak evidence" of correctness, because the program may contain the same bug. And on ties it supports exactly two responsible options — a public draw with an explicit --tie order, or a PRNG with a published seed — which is NFR-1 stated as product design.
That is the same conclusion this repo reached from the other direction with its own published lot_numbers:.
What they assume — the assumptions worth knowing before quoting any of them¶
| Assumption | vse-sim | IEVS | elsim | this repo's scripts |
|---|---|---|---|---|
| Electorate | hierarchical clusters, 40 voters, 6 candidates | 11 generators incl. real-election-derived | IC, spatial, normal | six Euclidean spaces + statistical cultures |
| Honest score ballot | per-voter min–max | per-voter min–max | configurable | per-voter min–max |
| Honest approval cutoff | midpoint of range | mean, coin toss at the mean | configurable | swept, >= 4 default |
| Voter self-knowledge | perfect (ignorance is at poll level only) | imperfect — AddIgnorance, constant or stratified |
n/a | perfect |
| Strategists | independent coin flips, not a coalition | fraction of voters, one strategy | n/a | modelled explicitly per script |
| Strategy fractions | 0/25/50/75/100 only | parameterized | n/a | per script |
| Ties | random.choice |
varies | varies | reported, never silently broken |
| Confidence intervals | computable, never printed | summarizer | — | per script |
The two rows to stare at are the approval cutoff and voter self-knowledge, because both are silent modelling choices that no chart label mentions and both move results. Two simulators disagreeing about what a sincere Approval ballot is will produce two different "Approval" lines from identical electorates, and neither is wrong.
What questions they posed¶
Worth reading as a list, because the questions are more transferable than the code:
- vse-sim / Quinn: how much of the achievable voter satisfaction does each method deliver, and how much does strategy take away? — with the answer expressed on a normalized 0–1 scale anchored at "random winner" and "best possible winner."
- IEVS / Smith: the same question, but across 68 methods and 11 electorates at once — the "Chinese menu," built so the comparison space is A×B×C×D for A+B+C+D of work.
- quadelect / Munsterhjelm: not "which of these methods is best" but "what is the best method in this space?" — searching over composed methods, and proving criterion failures by LP rather than finding them by sampling.
- elsim / endolith: what do the classic results actually look like when re-run? — reproduction and visualization as the goal, which is why it produced the center-squeeze animation.
- VMES / Ogren: what is the strategic incentive under each method (PVSI), and how do the multi-winner variants compare — the only one on this list asking both.
- mapof / Faliszewski et al.: is the electorate model even the right unit of analysis? — replacing "assume IC" with a map of where real and synthetic elections sit relative to each other.
- ConcreteSTV / Conway: does the official count match the law? — the only one whose findings changed legislation.
This library's own question is a seventh, and it is not on that list: how does one election look under every method at once — the same-opinion line-up. That is a case question, not a rate question, and it is why twelve scripts exist here rather than one simulator.
Part 6 — Should it be Rust? Measured¶
The kernel pages answered no for the counter, on the grounds that a test library's dominant cost is authoring. A simulator's dominant cost is running, so the argument has to be re-made rather than inherited — and this repo's rule is to measure rather than assert.
Setup. Identical minimal STAR kernel — sum the scores, take the top two, run the pairwise runoff — written once in Rust and once in Python, with ballot generation inside the timed loop in both. Intel Core i5-10500 @ 3.10 GHz, rustc 1.97.1 (opt-level=3, LTO, codegen-units=1), CPython 3.13.13, numpy 2.2.6. Single core throughout. The Rust source is at the bottom of this section so the numbers can be re-run.
Workload A — Monte-Carlo sampling (5 candidates, 501 voters)¶
| Implementation | elections/s/core | vs pure Python |
|---|---|---|
| pure Python | 1,227 | 1× |
| numpy, one election at a time | 27,323 | 22× |
| numpy, batched across elections | 66,049 | 54× |
| Rust | 191,100 | 156× |
(starvote.election, the real engine, 5 cands / 501 voters) |
348 | 0.3× |
Rust beats well-written numpy by 2.9×. That is the whole margin, and it is not a reason to change language — because numpy is already vectorizing across the 501 voters, which is where the work is.
Workload B — exhaustive enumeration (3 candidates, 7 ballots)¶
| Implementation | elections/s/core | vs pure Python |
|---|---|---|
| pure Python | 110,538 | 1× |
Rust, runtime dimensions (Vec) |
9,838,000 | 89× |
| Rust, compile-time dimensions (fixed arrays) | 26,260,000 | 238× |
(starvote.election, same shape) |
15,423 | 0.14× |
Rust beats Python by 238× — and note that the fixed-size version is 2.7× the Vec version, which is a design requirement in its own right: the inner loop must not allocate, and the dimensions want to be compile-time constants or const generics.
The finding: the case for Rust runs opposite to intuition¶
Big electorates favour Python, because numpy vectorizes across voters and there is real work per election to amortize the interpreter over. Tiny electorates in enormous numbers favour Rust, because per-election overhead is all there is and numpy's vectorization has nothing to bite on. The simulator Adam described — sampling realistic electorates — is workload A. The 238× number belongs to a different program.
And even for workload B, speed buys less than it looks¶
There are 4,792,382,636,184 distinct anonymous STAR profiles at 3 candidates, 7 ballots, scores 0–5. At the measured rates:
| Python | Rust | |
|---|---|---|
| 3 candidates, 7 ballots (4.79 × 10¹²) | 1.4 core-years | 2.1 core-days — about 6 hours on 8 cores, ~1 hour with the m! = 6 candidate-relabelling symmetry |
| 3 candidates, 9 ballots (3.33 × 10¹⁵) | 953 core-years | 4.0 core-years — ~4 months on 12 cores |
So Rust genuinely converts "impossible" into "an afternoon" at 7 ballots. But the profile count grows by ~26× per additional voter, so a 238× speedup buys fewer than two extra voters. Constant factors do not defeat combinatorial explosion. What does is symmetry reduction (a free 6× here), and what quadelect does instead: solve for counterexamples with linear programming rather than search for them. If S5 is the goal, the LP approach is worth more than the language.
And the shape matters as much as the size. The same enumeration for ranked ballots is trivial — 792 profiles at 3 candidates and 7 voters, against 4.79 trillion for score ballots, because a ranked ballot has 6 possible values and a 0–5 score ballot has 216. Exhaustive search over ranked methods needs no Rust at all; it is the score ballot that explodes.
The benchmark source (re-runnable)
// Minimal STAR kernel: 3 candidates, 7 ballots, scores 0-5.
const M: usize = 3;
const N: usize = 7;
#[inline(always)]
fn star_winner(b: &[[u8; M]; N]) -> usize {
let mut tot = [0u32; M]; // scoring round
for row in b.iter() { for c in 0..M { tot[c] += row[c] as u32; } }
let mut a = 0usize; // top two
for c in 1..M { if tot[c] > tot[a] { a = c; } }
let mut d = if a == 0 { 1 } else { 0 };
for c in 0..M { if c != a && tot[c] > tot[d] { d = c; } }
let (mut pa, mut pd) = (0u32, 0u32); // automatic runoff
for row in b.iter() {
if row[a] > row[d] { pa += 1 } else if row[d] > row[a] { pd += 1 }
}
if pa > pd { a } else if pd > pa { d } else if tot[a] >= tot[d] { a } else { d }
}
Part 7 — Recommendation¶
1. Fix vse-sim's reproduction script upstream. Two lines, already verified end-to-end, and it closes S1 without writing anything. It also puts a filed-and-fixed contribution against the canonical simulator on the record, which is worth more than a private fork. Details and the patch: a reproduction command nobody runs.
2. Evaluate VMES before building anything. It is the only project on this page that covers all six methods, its STARVoting(5) is the right ballot, it carries PVSI and a multi-winner quality metric, it is actively maintained, and it is written by a co-author of the paper this library already claim-checks. The honest first question is not "what language should our simulator be?" but "why is this not our simulator?" — and the answer might legitimately be Julia is a third language in a two-language repo, or its documentation is a Google Doc, or we want an independent implementation precisely so the numbers can disagree. Any of those is a fine answer. None of them can be given before someone runs it.
3. If something is built here, build it in Python, as a harness rather than a simulator. Workload A is 2.9× off Rust and the twelve scripts already contain most of the pieces. The valuable move is the "Chinese menu" refactor from Part 2: four independent stages, so that adding Bloc STAR costs one implementation rather than twelve. That work is worth doing whatever happens next, which is the ladder principle — order the work so that stopping is never a loss.
4. The one place Rust earns its keep is S5, and only for score ballots. 238×, measured, converting a 1.4-core-year enumeration into an afternoon. If that is the goal, scope it to exactly that: no models, no strategy, no metrics, no CLI — an enumerator, one method, an allocation-free kernel with compile-time dimensions, and a YAML emitter for the witness. That is a few hundred lines, it is a genuinely good Rust learning project, and it produces a publishable result rather than a chart. But read Part 6's last paragraph first: linear programming may beat brute force by more than Rust beats Python.
5. The multi-winner metric question (S4) is a page before it is a program. Which welfare measure for Bloc STAR and STAR-PR, and why — with the conflict between proportionality and utilitarian welfare stated rather than resolved by default. That page is worth writing whether or not any simulator follows it, and it is the piece that is missing from every project surveyed here.
Open questions¶
| # | Question | Leaning |
|---|---|---|
| Q-1 | Does VMES already do this, well enough? | Unknown, and answerable in an afternoon. Blocks everything else |
| Q-2 | Primary goal — S2 (measure our own claims) or S4 (multi-winner) or S5 (search)? | S2 as the default; S4 as the one with a publishable gap |
| Q-3 | Which multi-winner welfare metric? | State the conflict; report at least two |
| Q-4 | Rationals or floats in the PR family? | stv-rs's answer: floats make results ballot-order-dependent and block parallel reduction. Fixed-point is the middle road |
| Q-5 | Does the harness call starvote, or reimplement? |
Reimplement for speed, cross-check against starvote on a sample (FR-12). Calling it is 350 elections/s |
| Q-6 | One harness, or keep twelve scripts? | Keep the scripts; extract the four stages beneath them (NG-1) |
| Q-7 | Is any of this promised publicly before it exists? | No |
Related¶
- The simulations folder — the twelve scripts, what each measures, and the findings
- vse-sim, read from source · a reproduction command nobody runs — the canonical simulator, and its live bug
- Election simulation models · Simulate utilities, not ballots · The six Euclidean spaces · Statistical cultures — the modelling layer, already built and cross-checked
- A Rust voting kernel — goals and requirements · A Rust tabulation kernel — scope — the same question for the counter, decided no
- The result contract · Tiebreak ladders — what a second implementation has to match
- Formal compliance vs. strategic preservation · PVSI · Condorcet efficiency, measured — this library's existing measured results
Up: Simulations · 06_Other.