some formatting and add cli compilation

This commit is contained in:
Thomas (Tom) C. Gorordo 2026-05-20 12:04:17 -07:00
parent 8836c49091
commit 4a624a4847
Signed by: tgorordo
GPG key ID: 0CBED22BB0D94490
9 changed files with 115 additions and 61 deletions

View file

@ -13,8 +13,12 @@ test:
format: format:
uv run ruff format src test uv run ruff format src test
example:
uv run python src/main.py test/test_ballot.csv
compile: compile:
uv run pyinstaller src/main.py uv run pyinstaller --clean -F src/main.py --name smithy
clean: clean:
uv run pyclean src test uv run pyclean src test

View file

@ -14,7 +14,7 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
smithy = "smithy:main" smithy = "smithy:cli"
[build-system] [build-system]
requires = ["uv_build>=0.11.7,<0.12.0"] requires = ["uv_build>=0.11.7,<0.12.0"]

View file

@ -118,7 +118,9 @@ platformdirs==4.9.6
pluggy==1.6.0 pluggy==1.6.0
# via pytest # via pytest
polars==1.40.1 polars==1.40.1
# via marimo # via
# smithy (pyproject.toml)
# marimo
polars-runtime-32==1.40.1 polars-runtime-32==1.40.1
# via polars # via polars
psutil==7.2.2 psutil==7.2.2

38
smithy.spec Normal file
View file

@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['src/main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='smithy',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View file

@ -0,0 +1,4 @@
from smithy import cli
if __name__ == "__main__":
cli()

View file

@ -7,23 +7,20 @@ from rich.panel import Panel
from .rcv import smith_set from .rcv import smith_set
console = Console()
@click.command() @click.command()
@click.argument( @click.argument("spreadsheet", type=click.Path(exists=True, dir_okay=False))
"spreadsheet", def cli(spreadsheet: str) -> None:
type=click.Path(exists=True, dir_okay=False)
)
def main(spreadsheet: str) -> None:
""" """
Compute the Smith set from a ranked-choice ballot spreadsheet. Compute the Smith set from a ranked-choice ballot spreadsheet.
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the Condorcet i.e. Majority winner. in the set they are guaranteed the Condorcet i.e. Majority winner.
""" """
try: console = Console()
try:
# Load spreadsheet # Load spreadsheet
if spreadsheet.endswith(".csv"): if spreadsheet.endswith(".csv"):
df = pl.read_csv(spreadsheet) df = pl.read_csv(spreadsheet)
@ -33,17 +30,21 @@ def main(spreadsheet: str) -> None:
else: else:
console.print( console.print(
"[bold red]Unsupported file type.[/bold red]\n" "[bold red]Unsupported file type.[/bold red]\nUse CSV or Excel."
"Use CSV or Excel."
) )
raise SystemExit(1) raise SystemExit(1)
# Normalize numerical dataframe entries # Normalize numerical dataframe entries
df = df.with_columns([ pl.col(c) df = df.with_columns(
.cast(pl.Utf8) [
.str.strip_chars() pl.col(c)
.cast(pl.Int64, strict=False).fill_null(0) .cast(pl.Utf8)
for c in df.columns ]) .str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(0)
for c in df.columns
]
)
# Compute Smith set # Compute Smith set
smiths = smith_set(df) smiths = smith_set(df)
@ -66,14 +67,11 @@ def main(spreadsheet: str) -> None:
Panel.fit( Panel.fit(
"\n".join(f"{c}" for c in smiths), "\n".join(f"{c}" for c in smiths),
title="Resulting Smith Set", title="Resulting Smith Set",
border_style="green" border_style="green",
) )
) )
except Exception as e: except Exception as e:
console.print(f"[bold red]Error:[/bold red] {e}")
console.print(
f"[bold red]Error:[/bold red] {e}"
)
raise SystemExit(1) raise SystemExit(1)

View file

@ -1,13 +1,14 @@
import polars as pl import polars as pl
from itertools import combinations from itertools import combinations
def smith_set(df: pl.DataFrame) -> list: def smith_set(df: pl.DataFrame) -> list:
""" """
Compute the Smith set from a Ranked-Choice ballot. Compute the Smith set from a Ranked-Choice ballot.
The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner The Smith set is the minimal set of candidates which can beat all others pairwise - if there is a single winner
in the set they are guaranteed the Condorcet i.e. Majority winner. in the set they are guaranteed the Condorcet i.e. Majority winner.
parameters parameters
--- ---
df : pl.DataFrame df : pl.DataFrame
@ -27,7 +28,7 @@ def smith_set(df: pl.DataFrame) -> list:
candidates = df.columns candidates = df.columns
# Build pairwise majority graph # Build pairwise majority graph
graph: dict[str, set[str]] = { c: set() for c in candidates } graph: dict[str, set[str]] = {c: set() for c in candidates}
for a, b in combinations(candidates, 2): for a, b in combinations(candidates, 2):
result = df.select( result = df.select(
@ -46,17 +47,13 @@ def smith_set(df: pl.DataFrame) -> list:
# Find Smith set # Find Smith set
for size in range(1, len(candidates) + 1): for size in range(1, len(candidates) + 1):
for sub in combinations(candidates, size): for sub in combinations(candidates, size):
subset = set(sub) subset = set(sub)
out = set(candidates) - subset out = set(candidates) - subset
dom = True dom = True
for member in subset: for member in subset:
# DIRECT dominance only
if not out.issubset(graph[member]): if not out.issubset(graph[member]):
dom = False dom = False
break break

View file

@ -23,18 +23,23 @@ def _():
@app.cell @app.cell
def _(mo, pl): def _(mo, pl):
df = pl.read_csv(mo.notebook_dir() / "test_ballot.csv") df = pl.read_csv(mo.notebook_dir() / "test_ballot.csv")
df = df.with_columns([ pl.col(c) # make safe, clean up df = df.with_columns(
.cast(pl.Utf8) [
.str.strip_chars() pl.col(c) # make safe, clean up
.cast(pl.Int64, strict=False).fill_null(df.width + 1) .cast(pl.Utf8)
for c in df.columns ]) .str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(df.width + 1)
for c in df.columns
]
)
df df
return (df,) return (df,)
@app.cell @app.cell
def _(df, smith_set): def _(df, smith_set):
smith_set(df) # find the smith set (should be "Alice" and "Bob" as a pair) smith_set(df) # find the smith set (should be "Alice" and "Bob" as a pair)
return return

View file

@ -1,35 +1,41 @@
import polars as pl import polars as pl
from smithy import smith_set from smithy import smith_set
def test_condorcet(): def test_condorcet():
df = pl.DataFrame({ df = pl.DataFrame(
'A': [1, 1, 2, 1], {
'B': [2, 2, 1, 2], "A": [1, 1, 2, 1],
'C': [3, 3, 3, 3], "B": [2, 2, 1, 2],
}) "C": [3, 3, 3, 3],
assert smith_set(df) == ['A'] }
)
assert smith_set(df) == ["A"]
def test_rockpprscrcycle(): def test_rockpprscrcycle():
df = pl.DataFrame({ df = pl.DataFrame(
'A': [1, 2, 3], {
'B': [2, 3, 1], "A": [1, 2, 3],
'C': [3, 1, 2], "B": [2, 3, 1],
}) "C": [3, 1, 2],
assert smith_set(df) == ['A', 'B', 'C'] }
)
assert smith_set(df) == ["A", "B", "C"]
def test_abpair(): def test_abpair():
df = pl.DataFrame({ df = pl.DataFrame({"A": [1, 2, 1, 3], "B": [2, 1, 3, 1], "C": [3, 3, 2, 2]})
"A": [1, 2, 1, 3], assert smith_set(df) == ["A", "B"]
"B": [2, 1, 3, 1],
"C": [3, 3, 2, 2]
})
assert smith_set(df) == ['A', 'B']
def test_fourcycle(): def test_fourcycle():
df = pl.DataFrame({ df = pl.DataFrame(
"A": [1,2,3,4], {
"B": [2,3,4,1], "A": [1, 2, 3, 4],
"C": [3,4,1,2], "B": [2, 3, 4, 1],
"D": [4,1,2,3], "C": [3, 4, 1, 2],
}) "D": [4, 1, 2, 3],
assert smith_set(df) == ['A', 'B', 'C', 'D'] }
)
assert smith_set(df) == ["A", "B", "C", "D"]