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:
uv run ruff format src test
example:
uv run python src/main.py test/test_ballot.csv
compile:
uv run pyinstaller src/main.py
uv run pyinstaller --clean -F src/main.py --name smithy
clean:
uv run pyclean src test

View file

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

View file

@ -118,7 +118,9 @@ platformdirs==4.9.6
pluggy==1.6.0
# via pytest
polars==1.40.1
# via marimo
# via
# smithy (pyproject.toml)
# marimo
polars-runtime-32==1.40.1
# via polars
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,14 +7,10 @@ from rich.panel import Panel
from .rcv import smith_set
console = Console()
@click.command()
@click.argument(
"spreadsheet",
type=click.Path(exists=True, dir_okay=False)
)
def main(spreadsheet: str) -> None:
@click.argument("spreadsheet", type=click.Path(exists=True, dir_okay=False))
def cli(spreadsheet: str) -> None:
"""
Compute the Smith set from a ranked-choice ballot spreadsheet.
@ -22,8 +18,9 @@ def main(spreadsheet: str) -> None:
in the set they are guaranteed the Condorcet i.e. Majority winner.
"""
try:
console = Console()
try:
# Load spreadsheet
if spreadsheet.endswith(".csv"):
df = pl.read_csv(spreadsheet)
@ -33,17 +30,21 @@ def main(spreadsheet: str) -> None:
else:
console.print(
"[bold red]Unsupported file type.[/bold red]\n"
"Use CSV or Excel."
"[bold red]Unsupported file type.[/bold red]\nUse CSV or Excel."
)
raise SystemExit(1)
# Normalize numerical dataframe entries
df = df.with_columns([ pl.col(c)
.cast(pl.Utf8)
.str.strip_chars()
.cast(pl.Int64, strict=False).fill_null(0)
for c in df.columns ])
df = df.with_columns(
[
pl.col(c)
.cast(pl.Utf8)
.str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(0)
for c in df.columns
]
)
# Compute Smith set
smiths = smith_set(df)
@ -66,14 +67,11 @@ def main(spreadsheet: str) -> None:
Panel.fit(
"\n".join(f"{c}" for c in smiths),
title="Resulting Smith Set",
border_style="green"
border_style="green",
)
)
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)

View file

@ -1,6 +1,7 @@
import polars as pl
from itertools import combinations
def smith_set(df: pl.DataFrame) -> list:
"""
Compute the Smith set from a Ranked-Choice ballot.
@ -27,7 +28,7 @@ def smith_set(df: pl.DataFrame) -> list:
candidates = df.columns
# 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):
result = df.select(
@ -46,17 +47,13 @@ def smith_set(df: pl.DataFrame) -> list:
# Find Smith set
for size in range(1, len(candidates) + 1):
for sub in combinations(candidates, size):
subset = set(sub)
out = set(candidates) - subset
dom = True
for member in subset:
# DIRECT dominance only
if not out.issubset(graph[member]):
dom = False
break

View file

@ -23,18 +23,23 @@ def _():
@app.cell
def _(mo, pl):
df = pl.read_csv(mo.notebook_dir() / "test_ballot.csv")
df = df.with_columns([ pl.col(c) # make safe, clean up
.cast(pl.Utf8)
.str.strip_chars()
.cast(pl.Int64, strict=False).fill_null(df.width + 1)
for c in df.columns ])
df = df.with_columns(
[
pl.col(c) # make safe, clean up
.cast(pl.Utf8)
.str.strip_chars()
.cast(pl.Int64, strict=False)
.fill_null(df.width + 1)
for c in df.columns
]
)
df
return (df,)
@app.cell
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

View file

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