8.6 KiB
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.
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_64and linux onaarch64. Downloading the appropriate executable should give you access to thesmithycmdCLI command. -
Alternatively, if you are using
uv(see Development), you can invoke this in your shell asuv 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
uvaddingsmithyshould be as simple as
uv add git+https://github.com/tgorordo/smithy.git
- Alternatively, if you're using the more default
pipyou 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).
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.