From 18405644d34880a5dd085520f6000204f809bf35 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" Date: Mon, 25 May 2026 22:23:57 -0700 Subject: [PATCH 01/23] sync --- src/smithygui.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/smithygui.py b/src/smithygui.py index 389ce1c..cfd22e2 100644 --- a/src/smithygui.py +++ b/src/smithygui.py @@ -1,3 +1,13 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "click>=8.4.1", +# "polars>=1.41.0", +# "pyside6>=6.11.1", +# "rich>=15.0.0", +# "rustworkx>=0.17.1", +# ] +# /// from PySide6.QtWidgets import QApplication, QWidget app = QApplication(sys.argv) From d1db77c4666f32b512b7bc8cbada7c3aaea96034 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Mon, 25 May 2026 22:25:52 -0700 Subject: [PATCH 02/23] Revise README Updated README to improve clarity and add usage details. --- README.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eb398ea..a7d3f84 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Pairwise majority comparisons scale quadratically in the number of candidates an 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. +pairwise evaluation to improve performance over duplicate rankings. This is all overkill for small elections, but is fun. @@ -33,7 +33,22 @@ executable should give you access to the `smithycmd` CLI command. 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: -TODO +```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. +``` If you want a 'None' option in your election, it should be included as a candidate. @@ -49,9 +64,9 @@ hosts a [CGI form](https://service.uoregon.edu/TDClient/2030/Portal/KB/ArticleDe 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)` From 1a6de605ad295866d29e1a14bb6ad8b3ead8ea15 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Mon, 25 May 2026 22:28:32 -0700 Subject: [PATCH 03/23] Update README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index a7d3f84..3f815b4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ Options: -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. From 57caad7f27a63522e4f081ad618e64242952616a Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Mon, 25 May 2026 22:31:39 -0700 Subject: [PATCH 04/23] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f815b4..707216b 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,8 @@ If you want a 'None' option in your election, it should be included as a candida 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. +`--pretty` (or `-p`) flag. Whether to show the input ballots during output is also +controlled by a corresponding flag. ### UOregon CGI Application From f762fd5a23fd3da182a3111929cdffb26f94ffcc Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Mon, 25 May 2026 23:09:23 -0700 Subject: [PATCH 05/23] Update form.html --- src/cgi/form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cgi/form.html b/src/cgi/form.html index b66b756..40ec12b 100644 --- a/src/cgi/form.html +++ b/src/cgi/form.html @@ -87,7 +87,7 @@ e.g.:

where this reads that the first voter had preferences "Alice" over "Bob" over "Charlie", the second voter "Bob" over "Alice" over "Charlie", etc. -(This example is constructed to demonstrate a situation without a singular majority winner - its Smith set a tie between "Alice" and "Bob")

+(This example is constructed to demonstrate a situation without a singular majority winner - its Smith set a tie between "Alice" and "Bob"). Rankings may contain ties by e.g. repeating a numerical rank.

Output: Smith Set Format

The form will return a list of the Smith-set winners sorted lexicographically (they are all equally good majority winners). From 24243aad2f285ec675ed13b5a675cdaffe631de9 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Wed, 27 May 2026 23:04:51 -0700 Subject: [PATCH 06/23] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 707216b..c765103 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,9 @@ 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 From b24920c15a00f6910a9dc9fe7477ab1e528bb487 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:19:39 -0700 Subject: [PATCH 07/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c765103..b93a4a8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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). +guaranteed the standard [Condorcet i.e. Majority winner](https://en.wikipedia.org/wiki/Condorcet_winner) (they beat all others pairwise). (TODO: optionally resolve nontrivial Smith sets - ties - via plurality/IRV methods within the set). `smithy` identifies the Smith set via graph Strongly Connected Component (SCC) analysis of the pairwise majority graph using [`rustworkx`](https://www.rustworkx.org/). From 845ac38533d3a9f8869850bee2998683724ee627 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:30:02 -0700 Subject: [PATCH 08/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b93a4a8..3a06617 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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). (TODO: optionally resolve nontrivial Smith sets - ties - via plurality/IRV methods within the set). +guaranteed the standard [Condorcet i.e. Majority winner](https://en.wikipedia.org/wiki/Condorcet_winner) (they beat all others pairwise). (TODO: for small elections, optionally resolve nontrivial Smith sets - ties - via plurality methods within the set, at least reducing to something like e.g. an IRV winner set within the Smith set if not likely identifying a unique candidate who wins all paths). `smithy` identifies the Smith set via graph Strongly Connected Component (SCC) analysis of the pairwise majority graph using [`rustworkx`](https://www.rustworkx.org/). From 3dc8a3cb46a67a54cfc1529b7b4e801394e18a68 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:31:35 -0700 Subject: [PATCH 09/23] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a06617..36bd269 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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). (TODO: for small elections, optionally resolve nontrivial Smith sets - ties - via plurality methods within the set, at least reducing to something like e.g. an IRV winner set within the Smith set if not likely identifying a unique candidate who wins all paths). +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/). @@ -12,7 +12,9 @@ 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. +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 ties, optionally resolve nontrivial Smith sets - ties - via plurality methods within the set, at least reducing to something like e.g. an IRV winner set within the Smith set if not likely identifying a unique candidate who wins all paths). ## Usage From 55f64d1a323db37d5cf6af68ec69efbd174a72cf Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:34:54 -0700 Subject: [PATCH 10/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36bd269..22b80cb 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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 ties, optionally resolve nontrivial Smith sets - ties - via plurality methods within the set, at least reducing to something like e.g. an IRV winner set within the Smith set if not likely identifying a unique candidate who wins all paths). +(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 From fa24eb575833748d1806909585a12e1222778810 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:35:21 -0700 Subject: [PATCH 11/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22b80cb..fdc2f66 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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). +(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 From e9c263a8c529b65ea8a666c253bb623856c97e8a Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:48:24 -0700 Subject: [PATCH 12/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fdc2f66..b709203 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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). +(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 and builds a winning coalition willing to elect them as delegate). ## Usage From 17c121f0afd9b7126b34dbb8823c518601225fb3 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:04:49 -0700 Subject: [PATCH 13/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b709203..25edb9c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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 and builds a winning coalition willing to elect them as delegate). +(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 delegate who wins all paths, having built a majority coalition). ## Usage From bc146d523d88667eb3b93474f3d56e99d48b775f Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:23:05 -0700 Subject: [PATCH 14/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25edb9c..3246b3d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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 delegate who wins all paths, having built a majority coalition). +(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 majority coalition). ## Usage From 51d651bee61e641f2f88513cd3adb07558d32973 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:07:22 -0700 Subject: [PATCH 15/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3246b3d..df9c095 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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 majority coalition). +(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). ## Usage From fc0569e252d1c97c626fc698a96c0647ce9510a1 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:23:03 -0700 Subject: [PATCH 16/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df9c095..434ed18 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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). +(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 From e0d0134fddb3c27fa40352085056ad8ed835d9cc Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" Date: Fri, 5 Jun 2026 14:39:31 -0700 Subject: [PATCH 17/23] add IRV smith-set resolution method --- src/smithy/__init__.py | 10 ++++ src/smithy/irv.py | 102 +++++++++++++++++++++++++++++++++++++++++ src/smithy/rcv.py | 42 +++++++++++------ src/smithycmd.py | 18 ++++++-- 4 files changed, 153 insertions(+), 19 deletions(-) create mode 100644 src/smithy/irv.py diff --git a/src/smithy/__init__.py b/src/smithy/__init__.py index 61d9a84..ec9ec9f 100644 --- a/src/smithy/__init__.py +++ b/src/smithy/__init__.py @@ -3,6 +3,7 @@ import rustworkx as rwx from itertools import combinations from .rcv import pmg_from_rcv +from .irv import irv_from_rcv def ss_from_pmg(pmg: rwx.PyDiGraph) -> list[str]: @@ -67,3 +68,12 @@ def smith_set(df: pl.DataFrame, ballotkind="rcv") -> list: raise NotImplementedError( f"`smith_set` ballotkind={ballotkind} is not implemented." ) + + +def irv_set(df: pl.DataFrame, ballotkind="rcv") -> list: + if ballotkind == "rcv": + return irv_from_rcv(df) + else: + raise NotImplementedError( + f"`irv_set` ballotkind={ballotkind} is not implemented." + ) diff --git a/src/smithy/irv.py b/src/smithy/irv.py new file mode 100644 index 0000000..eb89612 --- /dev/null +++ b/src/smithy/irv.py @@ -0,0 +1,102 @@ +import polars as pl +import numpy as np + + +def irv_from_rcv(ballots: pl.DataFrame, method: str = "bigslow") -> list[str]: + """ + Compute the set of all-paths IRV winners from an RCV ballot. + + parameters + --- + ballots: pl.DataFrame + An RCV table of ballots. + + method: str + Either "bigslow" or "smallfast" for selecting an internal method for counting + first-choices during IRV rounds. Defaults to "bigslow" but you can use "smallfast" + so long as the table of ballots is expected to fit in a reasonable numpy array (after compression). + + returns + --- + winners: list[srt] + A lexicographically sorted list of IRV winners. If a candidate wins every elimination path + then this set will contain only one entry, otherwise it will contain all candidates that win + at least one IRV elimination path. + """ + compressed = ballots.group_by(ballots.columns).len().rename({"len": "count"}) + return sorted(_irv_winners(compressed, method=method)) + + +def _fst_counts_bigslow(compressed: pl.DataFrame) -> pl.DataFrame: + + surviving = [c for c in compressed.columns if c != "count"] + + fstcexpr = ( + pl.concat_list([pl.col(c) for c in surviving]) + .list.arg_min().map_elements(lambda i: surviving[i], return_dtype=pl.String).alias("first_choice") + ) + + tally = ( + compressed.with_columns(fstcexpr) + .group_by("first_choice") + .agg(pl.col("count").sum()) + .filter(pl.col("first_choice").is_not_null()) + ) + return tally + + +def _fst_counts_smallfast(compressed: pl.DataFrame) -> pl.DataFrame: + + surviving = [c for c in compressed.columns if c != "count"] + + a = compressed.select(surviving).to_numpy() + + cs = compressed["count"].to_numpy() + + fstc_idxs = np.argmin(a, axis=1) + + tally = {c: 0 for c in surviving} + for i, c in zip(fstc_idxs, cs): + tally[surviving[i]] += int(c) + + return pl.DataFrame( + {"first_choice": surviving, "count": [tally[c] for c in surviving]} + ) + + +def _irv_round(compressed: pl.DataFrame, method="bigslow"): + + if method == "bigslow": + count_fn = _fst_counts_bigslow + elif method == "smallfast": + count_fn = _fst_counts_smallfast + else: + raise NotImplementedError( + f"Error: _fst_counts method={method} not implemented." + ) + + tally = count_fn(compressed) + + eliminate = tally.filter(pl.col("count") == pl.col("count").min())[ + "first_choice" + ].to_list() + + for e in eliminate: + surviving = [c for c in compressed.columns if c not in ("count", e)] + yield ( + compressed.select(surviving + ["count"]) + .group_by(surviving) + .agg(pl.col("count").sum()) + ) + + +def _irv_winners(compressed, method="bigslow"): + + surviving = [c for c in compressed.columns if c != "count"] + if len(surviving) == 1: + return set(surviving) + + winners = set() + for branch in _irv_round(compressed, method=method): + winners |= _irv_winners(branch, method=method) + return winners diff --git a/src/smithy/rcv.py b/src/smithy/rcv.py index 99de429..4f51fe2 100644 --- a/src/smithy/rcv.py +++ b/src/smithy/rcv.py @@ -3,7 +3,7 @@ import rustworkx as rwx from itertools import combinations -def pmg_from_rcv_polars(ballots: pl.DataFrame) -> rwx.PyDiGraph: +def pmg_from_rcv_bigslow(ballots: pl.DataFrame) -> rwx.PyDiGraph: """ Build a pairwise majority winner graph from a box of Ranked-Choice Ballots. @@ -34,10 +34,20 @@ def pmg_from_rcv_polars(ballots: pl.DataFrame) -> rwx.PyDiGraph: pairs = list(combinations(candidates, 2)) for a, b in pairs: - exprs.extend([ - pl.when(pl.col(a) < pl.col(b)).then(pl.col("count")).otherwise(0).sum().alias(f"{a}>{b}"), - pl.when(pl.col(b) < pl.col(a)).then(pl.col("count")).otherwise(0).sum().alias(f"{b}>{a}") - ]) + exprs.extend( + [ + pl.when(pl.col(a) < pl.col(b)) + .then(pl.col("count")) + .otherwise(0) + .sum() + .alias(f"{a}>{b}"), + pl.when(pl.col(b) < pl.col(a)) + .then(pl.col("count")) + .otherwise(0) + .sum() + .alias(f"{b}>{a}"), + ] + ) results = compressed.select(exprs).row(0, named=True) @@ -52,7 +62,8 @@ def pmg_from_rcv_polars(ballots: pl.DataFrame) -> rwx.PyDiGraph: return pmg -def pmg_from_rcv_numpy(ballots: pl.DataFrame) -> rwx.PyDiGraph: + +def pmg_from_rcv_smallfast(ballots: pl.DataFrame) -> rwx.PyDiGraph: """ Build a pairwise majority winner graph from a box of Ranked-Choice Ballots. @@ -79,17 +90,17 @@ def pmg_from_rcv_numpy(ballots: pl.DataFrame) -> rwx.PyDiGraph: compressed = ballots.group_by(ballots.columns).len().rename({"len": "count"}) counts = compressed["count"].to_numpy() - + arr = compressed.drop("count").to_numpy() results = ((arr[:, :, None] < arr[:, None, :]) * counts[:, None, None]).sum(axis=0) for i, a in enumerate(candidates): for j in range(i + 1, len(candidates)): b = candidates[j] - + a_wins = results[i, j] b_wins = results[j, i] - + if a_wins > b_wins: pmg.add_edge(nodes[a], nodes[b], int(a_wins - b_wins)) elif b_wins > a_wins: @@ -97,10 +108,11 @@ def pmg_from_rcv_numpy(ballots: pl.DataFrame) -> rwx.PyDiGraph: return pmg -def pmg_from_rcv(ballots: pl.DataFrame, method="numpy") -> rwx.PyDiGraph: - if method == "polars": - return pmg_from_rcv_polars(ballots) - elif method == "numpy": - return pmg_from_rcv_numpy(ballots) + +def pmg_from_rcv(ballots: pl.DataFrame, method="bigslow") -> rwx.PyDiGraph: + if method == "bigslow": + return pmg_from_rcv_bigslow(ballots) + elif method == "smallfast": + return pmg_from_rcv_smallfast(ballots) else: - raise NotImplementedError(f"`pmg_from_rcv` method={method} not implemented.") \ No newline at end of file + raise NotImplementedError(f"`pmg_from_rcv` method={method} not implemented.") diff --git a/src/smithycmd.py b/src/smithycmd.py index 3b58971..d94521b 100644 --- a/src/smithycmd.py +++ b/src/smithycmd.py @@ -15,11 +15,16 @@ from rich.table import Table from rich.panel import Panel import polars as pl -from smithy import smith_set +from smithy import smith_set, irv_set @click.command() @click.argument("ballots", type=click.Path(exists=True, dir_okay=False)) +@click.option( + "--try-resolve-irv", + is_flag=True, + help="Try to reduce or resolve the Smith set by running all-paths IRV on the set.", +) @click.option( "--show-ballots", "-b", @@ -27,7 +32,7 @@ from smithy import smith_set help="Show relevant ballots (after selections).", ) @click.option("--pretty", "-p", is_flag=True, help="Pretty-print output.") -def cli(ballots: str, show_ballots=False, pretty=False) -> None: +def cli(ballots: str, try_resolve_irv=False, show_ballots=False, pretty=False) -> None: """ Compute the Smith set from a box of ranked-choice ballots -- .csv or .xls(x). @@ -56,7 +61,7 @@ def cli(ballots: str, show_ballots=False, pretty=False) -> None: df = df.with_columns( [ pl.col(c) - .cast(pl.Utf8) + .cast(pl.String) .str.strip_chars() .cast(pl.Int64, strict=False) .fill_null(0) @@ -66,6 +71,9 @@ def cli(ballots: str, show_ballots=False, pretty=False) -> None: # Compute Smith set smiths = smith_set(df) + if len(smiths) > 1 and try_resolve_irv: + irv_ballots = df.select(smiths) + smiths = irv_set(irv_ballots) if show_ballots and pretty: preview = Table(title="Ballot Box") @@ -88,7 +96,9 @@ def cli(ballots: str, show_ballots=False, pretty=False) -> None: console.print( Panel.fit( "\n".join(f"• {c}" for c in smiths), - title="Resulting Smith Set", + title="Resulting IRV-resolved Smith Set" + if (try_resolve_irv) + else "Resulting Smith Set", border_style="green", ) ) From c324dc9c9f62292ec9cf4ae2ee9f78f9a1f732c6 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:48:08 -0700 Subject: [PATCH 18/23] Update README --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 434ed18..92df010 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ 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). +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 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 @@ -105,8 +106,8 @@ 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 From 6fcdae879231383c745612e41cfdb66c5941d256 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:49:08 -0700 Subject: [PATCH 19/23] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92df010..64c0964 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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 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. +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 From 59ed05e35ca12e272efcc6fd2de8390569a02198 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Sat, 6 Jun 2026 08:02:08 -0700 Subject: [PATCH 20/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64c0964..088e918 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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. +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. ## Usage From 218ab2cf6b80058a314b4a2ad510a90c91252b22 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Sat, 6 Jun 2026 08:23:38 -0700 Subject: [PATCH 21/23] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 088e918..008c0e6 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ of Condorcet elections. Internally, repeated ballots are compressed/cache-counte 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. +Optionally, `smithy` can try to further resolve a nontrivial Smith set (a majoritarian tie or cycle) by running all-tie-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 generated by branching on ties) 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 tied elimination paths) which can claim a plurality coalition amongst the majority winners. ## Usage From 8a324243b30072f52fd44c0b40f5eb9e1db8e321 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Sat, 6 Jun 2026 09:04:52 -0700 Subject: [PATCH 22/23] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 008c0e6..120f80f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 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-tie-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 generated by branching on ties) 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 tied elimination paths) which can claim a plurality coalition amongst the majority winners. +coalitions within the set; in practice this often resolves cycles and is likely to result in a unique delegate (if they win all IRV tied elimination paths) which can claim a plurality coalition amongst the majority winners. (Depending on your elections and priorities it may be preferable to allow actual coalitions to form, reducing the field of candidates, and re-run the vote than to use this automatic resolution). ## Usage From 38c2845c3c5b201967a3bd4433fb87c33851b764 Mon Sep 17 00:00:00 2001 From: "Thomas (Tom) C. Gorordo" <57684088+tgorordo@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:05:27 -0700 Subject: [PATCH 23/23] Update rcv.py to handle ties correctly --- src/smithy/rcv.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/smithy/rcv.py b/src/smithy/rcv.py index 4f51fe2..120d095 100644 --- a/src/smithy/rcv.py +++ b/src/smithy/rcv.py @@ -59,6 +59,9 @@ def pmg_from_rcv_bigslow(ballots: pl.DataFrame) -> rwx.PyDiGraph: pmg.add_edge(nodes[a], nodes[b], a_wins - b_wins) elif b_wins > a_wins: pmg.add_edge(nodes[b], nodes[a], b_wins - a_wins) + else: # tie + pmg.add_edge(nodes[a], nodes[b], 0) + pmg.add_edge(nodes[b], nodes[a], 0) return pmg @@ -105,6 +108,9 @@ def pmg_from_rcv_smallfast(ballots: pl.DataFrame) -> rwx.PyDiGraph: pmg.add_edge(nodes[a], nodes[b], int(a_wins - b_wins)) elif b_wins > a_wins: pmg.add_edge(nodes[b], nodes[a], int(b_wins - a_wins)) + else: # tie + pmg.add_edge(nodes[a], nodes[b], 0) + pmg.add_edge(nodes[b], nodes[a], 0) return pmg