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