7.2 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 currently identifies the set by brute-force search which is combinatoric complexity
in the worst case (TODO: better algorithm)
but appears approximately O(n^2) on-average in the number of candidates for typical/random ballots,
and linear in the number of ballots.
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: TODO
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.
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 (pathological cases, involving
many or large cycles, or especially a large cyclic tie, may perform worse and are not extensively tested).
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 with PyInstaller can be done by
uv run pyinstaller --clean -F src/cmd.py --name smithycmd
(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.