A simple Smith set solver for ranked-choice ballots. https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/form.html
Find a file
Thomas (Tom) C. Gorordo 55f64d1a32
Update README.md
2026-06-04 12:34:54 -07:00
src Update form.html 2026-05-25 23:09:23 -07:00
test switch default to numpy pmg building 2026-05-25 18:01:26 -07:00
.envrc add envrc 2026-05-25 18:40:32 -07:00
.gitignore add envrc 2026-05-25 18:40:32 -07:00
justfile version bump to 0.2.0 2026-05-25 19:09:38 -07:00
pyproject.toml version bump to 0.2.0 2026-05-25 19:09:38 -07:00
README.md Update README.md 2026-06-04 12:34:54 -07:00
requirements.txt upgrade deps 2026-05-25 07:18:40 -07:00
shell.nix init. core algorithm, initial testing, basic cli. 2026-05-20 11:39:03 -07:00
uv.lock version bump to 0.2.0 2026-05-25 19:09:38 -07:00

Smithy

A simple Smith set solver for ranked-choice ballots.

The Smith set is the minimal set of election candidates which can beat all others pairwise (by simple majority ranking preference) - if there is a single winner in the set they are guaranteed the standard Condorcet i.e. Majority winner (they beat all others pairwise).

smithy identifies the Smith set via graph Strongly Connected Component (SCC) analysis of the pairwise majority graph using rustworkx. Pairwise majority comparisons scale quadratically in the number of candidates and linearly in the number of ballots, while the SCC and condensation graph analysis is approximately quadratic in the number of candidates for the dense tournament graphs typical of Condorcet elections. Internally, repeated ballots are compressed/cache-counted before pairwise evaluation to improve performance over duplicate rankings. This is all overkill for small elections, but is fun.

(TODO: for small elections because enumerating all IRV paths scales badly in the event of many tied counts, optionally resolve nontrivial Smith sets - majoritarian ties - via IRV within the set, at least reducing to an IRV winner set within the Smith set if not likely identifying a unique candidate who wins all paths).

Usage

smithy can be used a few different ways: as a CLI command, via a web form upload, or imported as a python package (TODO: a small GUI).

CLI Command

smithy provides a click CLI command defined in src/smithycmd.py which can be invoked a couple of ways:

  • The GitHub Releases Page provides standalone CLI executables bundled using PyInstaller for linux and windows on x86_64 and linux on aarch64. Downloading the appropriate executable should give you access to the smithycmd CLI command.

  • Alternatively, if you are using uv (see Development), you can invoke this in your shell as uv run src/smithycmd.py [...] from within the repo after cloning it locally.

In either case, the command expects the same argument structure:

$ uv run src/smithycmd.py --help
Usage: smithycmd.py [OPTIONS] BALLOTS

  Compute the Smith set from a box of ranked-choice ballots -- .csv or
  .xls(x).

  The Smith set is the minimal set of candidates which can beat all others
  pairwise (simple ranking majority) - if there is a single winner in the set,
  they are guaranteed the Condorcet i.e. Majority winner.

Options:
  -b, --show-ballots  Show relevant ballots (after selections).
  -p, --pretty        Pretty-print output.
  --help              Show this message and exit.

where the ballots file should be a .csv or .xls(x) whose columns are candidates and whose rows are numerical RCV ballot rankings (lower number is best, 1st, 2nd, 3rd, etc.). (see the web deployment or test/ for some examples).

If you want a 'None' option in your election, it should be included as a candidate.

While plain output is the default, so that the command can easily be used in a unix-pipe or stdio workflows, it can also pretty-print its output for your reading pleasure using rich if you pass it the --pretty (or -p) flag. Whether to show the input ballots during output is also controlled by a corresponding flag.

UOregon CGI Application

This pages.uoregon.edu page hosts a CGI form which accepts a .csv or .xls(x) upload of a ballot-box and responds with the resulting Smith set. The application can typically handle ~100s of candidates and ~10ks of votes before running into hosting resource limitations and timing out.

No ballot data is retained on the server after execution.

Python Package

The smithy python package primarily provides the function smith_set(ballot_box_df) (defined in src/smithy), which returns the smith set given a polars dataframe ballot_box_df whose columns are candidates and whose rows are RCV ballots.

e.g. smithy can be imported into a marimo notebook as seen in test/test_nb.py - if you're using uv you can launch marimo via uv run marimo --edit and navigate to that example notebook (just marimo is an available shorthand if you are using the just taskrunner -- see Development). Cloning smithy then working in scratch notebooks within the repo (e.g. makeing your own nbs/ directory) is probably the easiest way to use it if you need to do some of cleanup of the tabular data before invocation - just do your cleanup in a notebook using polars then pass a tidy dataframe to smith_set(df).

Of course, you can also import into any python script or package for a more involved project as well:

  • If you're using uv adding smithy should be as simple as
uv add git+https://github.com/tgorordo/smithy.git
  • Alternatively, if you're using the more default pip you can make it available by
git clone https://github.com/tgorordo/smithy.git # or submodule add somewhere appropriate in your project
cd smithy
pip install .

(if you're new to pip-venvs, this brief guide might be helpful).

(The core algorithm is also pretty dead simple, and you could just copy it over into your project too).

Development

Current development tooling is based on the uv Python package and project manager. A justfile lists some common useful task commands. A simple shell.nix can be used to load these tools into a local environment, and you might find direnv a convenient pairing to nix - but just having a system-provided uv available is more than enough to use and develop smithy.

Formatting, Checking, Testing and Locking

Source can be formatted using ruff by running

uv run ruff format src test

(or just format) which should be done before any git commit.

This is not really taken full advantage of at the moment, but type hints can be checked by running pyright

uv run pyright src

(or just check).

A pytest test suite can be run as

uv run pytest -vvv --tb=short --log-cli-level=INFO

(or just test).

Dependencies can be locked by running

uv lock
uv pip compile pyproject.toml -o requirements.txt --group dev

(or just lock).

Bundling, Releases, Deployment and Cleanup

Bundling the CLI command with PyInstaller can be done by

uv run --with-requirements src/smithycmd.py pyinstaller --clean -F src/smithycmd.py

(or just compile) which will output an executable smithycmd for the CLI in dist/, which can be uploaded to the latest GitHub Releases Page.

Deploying the main CGI page at pages.uoregon.edu/tgorordo/files/smithy/src/cgi requires shell or SFTP access to my pages.uoregon.edu, which you won't get unless you're me.

If you want to deploy it to your own pages.uoregon.edu page, then it should be enough to ensure you have CGI set up then SFTP into sftp.uoregon.edu and put -r smithy (or git clone when sshed into shell.uoregon.edu) wherever you'd like in your public_html, ensure smithy.cgi has chmod 755 permissions, standalone install uv for your user then run uv sync from somewhere inside the smithy directory. Navigating to the smithy.cgi file in pages.uoregon.edu/YOUR_USER/.../cgi/smithy.cgi should give you a working form.

If you're using just you can cleanup working files by running just clean and can fully wipe your uv generated .venv with just wipe (if you want manual versions of those, check the justfile definition). It's probably best to do this before any commit as well, though probably not required if you're careful or have .gitignore updated appropriately for any changes you made.