mirror of
https://github.com/tgorordo/smithy.git
synced 2026-06-05 16:22:15 -07:00
179 lines
9.3 KiB
Markdown
179 lines
9.3 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.
|
|
|
|
(TODO: for small elections -because enumerating all tie resolution 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 delegate who wins all paths, having built 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 amongst the smith set 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.
|
|
|