smithy/README.md
Thomas (Tom) C. Gorordo 6fcdae8792
Update README
2026-06-05 14:49:08 -07:00

180 lines
9.5 KiB
Markdown

# Smithy
*A simple Smith set solver for ranked-choice ballots.*
The [Smith set](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/Condorcet_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`](https://www.rustworkx.org/).
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.
Optionally, `smithy` can try to further resolve a nontrivial Smith set (a majoritarian tie or cycle) by running all-paths IRV within the set - at least reducing to an IRV winner set (the set of candidates that win at least one IRV elimination path) within the Smith set that are not only pairwise competitive but can also build competitive plurality
coalitions within the set; in practice this often resolves cycles and is likely to result in a unique delegate (if they win all IRV elimination paths) which can claim a plurality coalition amongst the majority winners. This should identify a winning candidate as the best possible focal point for voluntary coordination; if your elections have other priorities you may want to resolve nontrivial Smith sets differently.
## 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](https://doc.qt.io/qtforpython-6/)).
### CLI Command
`smithy` provides a [`click`](https://click.palletsprojects.com/en/stable/)
CLI command defined in `src/smithycmd.py` which can be invoked a couple of ways:
- The [GitHub Releases Page](https://github.com/tgorordo/smithy/releases) provides standalone
CLI executables bundled using [PyInstaller](https://pyinstaller.org/en/stable/index.html)
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](#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:
```bash
$ 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](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/form.html)
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`](https://rich.readthedocs.io/en/stable/introduction.html) 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](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi/form.html)
hosts a [CGI form](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069)
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](https://docs.pola.rs/api/python/stable/reference/index.html)
dataframe `ballot_box_df` whose columns are candidates and whose rows are RCV ballots.
e.g. `smithy` can be imported into a [marimo](https://docs.marimo.io/#highlights) 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](#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](https://docs.pola.rs/py-polars/html/reference/) 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
```bash
uv add git+https://github.com/tgorordo/smithy.git
```
- Alternatively, if you're using the more default `pip` you can make it available by
```bash
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](https://pages.uoregon.edu/tgorordo/courses/uoph410-510a_Image-Analysis/setup.html)).
(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`](https://docs.astral.sh/uv/) Python package and
project manager. A [`justfile`](https://github.com/casey/just) lists some common useful
task commands. A simple [`shell.nix`](https://nix.dev/tutorials/first-steps/declarative-shell.html)
can be used to load these tools into a local environment, and you might find
[`direnv`](https://github.com/direnv/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`](https://docs.astral.sh/ruff/) by running
```bash
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](https://github.com/microsoft/pyright)
```bash
uv run pyright src
```
(or `just check`).
A [pytest](https://docs.pytest.org/en/stable/) test suite can be run as
```bash
uv run pytest -vvv --tb=short --log-cli-level=INFO
```
(or `just test`).
Dependencies can be locked by running
```bash
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](https://pyinstaller.org/en/stable/index.html) can be done by
```bash
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](https://github.com/tgorordo/smithy/releases).
Deploying the main CGI page at
[`pages.uoregon.edu/tgorordo/files/smithy/src/cgi`](https://pages.uoregon.edu/tgorordo/files/smithy/src/cgi)
requires shell or SFTP access to [my `pages.uoregon.edu`](https://pages.uoregon.edu/tgorordo),
which you won't get unless you're [me](https://github.com/tgorordo).
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](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDet?ID=43069)
then SFTP into `sftp.uoregon.edu` and `put -r smithy` (or git clone when `ssh`ed 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](https://docs.astral.sh/uv/getting-started/installation/)
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.