Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions examples/run_ees_addopt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Running example for all algorithms in pabutools.rules.ees_addopt.

Demonstrates:
1. exact_equal_shares (Algorithm 1 – EES)
2. get_leftover_budgets (leftover budget helper)
3. get_leximax_payment (leximax payment helper)
4. greedy_project_change (Algorithm 2 – GPC)
5. add_opt (Algorithm 3 – add-opt)
6. ees_add_opt_completion (EES completion via add-opt)
"""

import logging

from pabutools.election import Instance, Project, ApprovalProfile, ApprovalBallot
from pabutools.fractions import frac
from pabutools.rules.budgetallocation import BudgetAllocation
from pabutools.rules.ees_addopt import (
EESAllocationDetails,
exact_equal_shares,
get_leftover_budgets,
get_leximax_payment,
greedy_project_change,
add_opt,
ees_add_opt_completion,
)

# Show all log messages from the module.
logging.basicConfig(
level=logging.DEBUG,
format="%(name)s %(levelname)s %(message)s",
stream=__import__("sys").stdout,
)

SEPARATOR = "\n" + "=" * 70 + "\n"


# A small PB instance with 5 voters and 3 projects.
# Projects: p1 (cost 2), p2 (cost 3.2), p3 (cost 6)
# Budget: 10
# Voter 0 approves {p1}
# Voter 1 approves {p1, p3}
# Voter 2 approves {p2, p3}
# Voter 3 approves {p2, p3}
# Voter 4 approves {p3}

p1 = Project("p1", 2)
p2 = Project("p2", 3.2)
p3 = Project("p3", 6)
projects = [p1, p2, p3]
budget = 10

instance = Instance(projects, budget_limit=budget)
profile = ApprovalProfile(
[
ApprovalBallot([p1]),
ApprovalBallot([p1, p3]),
ApprovalBallot([p2, p3]),
ApprovalBallot([p2, p3]),
ApprovalBallot([p3]),
],
instance=instance,
)

num_voters = len(profile)
print("Instance")
print(f" Budget : {budget}")
print(f" Projects : {[(p.name, p.cost) for p in projects]}")
print(f" Num voters : {num_voters}")
print(f" Approvals :")
for i, ballot in enumerate(profile):
print(f" Voter {i}: {sorted(p.name for p in ballot)}")


# 1. exact_equal_shares (Algorithm 1)
print(SEPARATOR)
print("1. exact_equal_shares (Algorithm 1 – EES)")
print("-" * 42)

ees_result = exact_equal_shares(instance, profile)

print(f"\n Selected projects : {[p.name for p in ees_result]}")
total_cost = sum(frac(p.cost) for p in ees_result)
print(f" Total cost : {total_cost}")

payments = ees_result.details.payments
print(" Per-voter payments:")
for voter in range(num_voters):
voter_pay = payments.get(voter, {})
items = [(p.name, float(v)) for p, v in voter_pay.items()]
print(f" Voter {voter}: {items if items else '(none)'}")


# 2. get_leftover_budgets
print(SEPARATOR)
print("2. get_leftover_budgets")
print("-" * 42)

leftover = get_leftover_budgets(instance, profile, ees_result)

print(" Leftover budget per voter:")
for voter in range(num_voters):
print(f" Voter {voter}: {float(leftover[voter]):.4f}")


# 3. get_leximax_payment
print(SEPARATOR)
print("3. get_leximax_payment")
print("-" * 42)

leximax = get_leximax_payment(ees_result, num_voters, instance)

print(" Leximax payment vectors:")
for voter in range(num_voters):
formatted = [(float(amt), name) for amt, name in leximax[voter]]
print(f" Voter {voter}: {formatted}")


# 4. greedy_project_change (Algorithm 2 – GPC)
print(SEPARATOR)
print("4. greedy_project_change (Algorithm 2 – GPC)")
print("-" * 42)

print(" Testing each project as instability certificate:")
for proj in projects:
d = greedy_project_change(
instance, profile, ees_result, proj, leftover, leximax
)
print(f" Project '{proj.name}' (cost={proj.cost}): d = {d} ({float(d):.4f})")


# 5. add_opt (Algorithm 3)
print(SEPARATOR)
print("5. add_opt (Algorithm 3)")
print("-" * 42)

d_min = add_opt(instance, profile, ees_result)

print(f"\n Minimum d over all projects: {d_min} ({float(d_min):.4f})")
print(f" Per-voter budget increase : {float(d_min):.4f}")
print(f" Total budget increase (n*d): {float(num_voters * d_min):.4f}")


# 6. ees_add_opt_completion (EES completion via add-opt)
print(SEPARATOR)
print("6. ees_add_opt_completion (EES completed via add-opt)")
print("-" * 42)

completed = ees_add_opt_completion(instance, profile)

print(f"\n Selected projects : {sorted(p.name for p in completed)}")
completed_cost = sum(frac(p.cost) for p in completed)
print(f" Total cost : {completed_cost} (budget = {budget})")

if hasattr(completed.details, "payments"):
print(" Per-voter payments:")
for voter in range(num_voters):
voter_pay = completed.details.payments.get(voter, {})
items = [(p.name, float(v)) for p, v in voter_pay.items()]
print(f" Voter {voter}: {items if items else '(none)'}")


# Comparison
print(SEPARATOR)
print("Summary comparison")
print("-" * 42)
print(f" EES alone : {str([p.name for p in ees_result]):30s} cost = {float(total_cost)}")
print(f" EES + add-opt : {str(sorted(p.name for p in completed)):30s} cost = {float(completed_cost)}")
Empty file added experiments/__init__.py
Empty file.
184 changes: 184 additions & 0 deletions experiments/experiment_comparison.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""
Section A – Performance comparison of EES algorithms vs. other PB rules.

Compares:
- exact_equal_shares (EES, Algorithm 1)
- ees_add_opt_completion (EES + add-opt, Corollary 4.7)
- method_of_equal_shares (MES, the standard library implementation)
- greedy_utilitarian_welfare (Greedy welfare)

Metrics:
- runtime (seconds)
- total_cost (budget utilisation)
- num_selected (number of selected projects)
- social_welfare (total approval score of selected projects)

Uses the experiments-csv library.
"""

import logging
import sys
import time

import experiments_csv

# ── make the repo root importable ──────────────────────────────────────
sys.path.insert(0, ".")

from pabutools.election import (
Instance,
Project,
ApprovalProfile,
ApprovalBallot,
Cost_Sat,
)
from pabutools.election.instance import get_random_instance
from pabutools.election.profile.approvalprofile import get_random_approval_profile
from pabutools.rules import greedy_utilitarian_welfare, method_of_equal_shares
from pabutools.rules.ees_addopt import exact_equal_shares, ees_add_opt_completion

TIME_LIMIT = 60 # seconds per single run

# ── algorithm registry ─────────────────────────────────────────────────
ALGORITHMS = {
"EES": lambda inst, prof: exact_equal_shares(inst, prof),
"EES_AddOpt": lambda inst, prof: ees_add_opt_completion(inst, prof),
"MES": lambda inst, prof: method_of_equal_shares(
inst, prof, sat_class=Cost_Sat, resoluteness=True
),
"Greedy": lambda inst, prof: greedy_utilitarian_welfare(
inst, prof, sat_class=Cost_Sat, resoluteness=True
),
}


# ── single experiment function ─────────────────────────────────────────
def run_single(num_projects: int, num_voters: int, algorithm: str, seed: int):
"""Run a single experiment: generate a random instance, run the algorithm,
and return measured outputs."""
import random as _random

_random.seed(seed)

# Generate random instance (same style as test_ees_addopt random tests)
approval_prob = 0.4
budget_factor_low, budget_factor_high = 0.4, 0.8

projects = []
for i in range(num_projects):
projects.append(Project(str(i), _random.randint(0, 100)))
total_project_cost = sum(p.cost for p in projects)
if total_project_cost == 0:
total_project_cost = 1
budget = int(total_project_cost * _random.uniform(budget_factor_low, budget_factor_high))
budget = max(budget, 1)
instance = Instance(projects, budget_limit=budget)

ballots = []
for v in range(num_voters):
approved = [p for p in projects if _random.random() < approval_prob]
ballots.append(ApprovalBallot(approved))
profile = ApprovalProfile(ballots, instance=instance)

algo_fn = ALGORITHMS[algorithm]

start = time.perf_counter()
result = algo_fn(instance, profile)
elapsed = time.perf_counter() - start

total_cost = sum(p.cost for p in result)
num_selected = len(result)

# Social welfare: total number of approvals for the selected projects
social_welfare = 0
for project in result:
social_welfare += profile.approval_score(project)

return {
"runtime": round(elapsed, 4),
"total_cost": int(total_cost),
"num_selected": num_selected,
"social_welfare": social_welfare,
"budget_limit": int(instance.budget_limit),
"remaining_budget": int(instance.budget_limit) - int(total_cost),
}


# ── main ───────────────────────────────────────────────────────────────
RESULTS_DIR = "experiments/results"
CSV_FILE = "comparison.csv"
BACKUPS_DIR = "experiments/results/backups"


def run_experiments():
"""Run the full comparison experiment."""
ex = experiments_csv.Experiment(RESULTS_DIR, CSV_FILE, BACKUPS_DIR)
ex.logger.setLevel(logging.INFO)

input_ranges = {
"num_projects": [10, 30, 60, 100],
"num_voters": [50],
"algorithm": list(ALGORITHMS.keys()),
"seed": [1, 2, 3],
}

ex.run(run_single, input_ranges)
print(f"\nExperiment data saved to {RESULTS_DIR}/{CSV_FILE}")


def plot_results():
"""Generate comparison plots from the CSV results."""
import pandas as pd
from matplotlib import pyplot as plt

csv_path = f"{RESULTS_DIR}/{CSV_FILE}"
df = pd.read_csv(csv_path)

# Compute remaining_budget if not already in CSV (backward compat)
if "remaining_budget" not in df.columns:
df["remaining_budget"] = df["budget_limit"] - df["total_cost"]

VOTER_COUNTS = [50]
metrics = [
("runtime", "Runtime (seconds)"),
("total_cost", "Total Cost"),
("remaining_budget", "Remaining Budget (unused)"),
("social_welfare", "Social Welfare (total approvals)"),
("num_selected", "Number of Selected Projects"),
]

for metric, ylabel in metrics:
for n_voters in VOTER_COUNTS:
plt.figure(figsize=(8, 5))
subset = df[df["num_voters"] == n_voters]
grouped = subset.groupby(["num_projects", "algorithm"])[metric].mean().reset_index()

for algo in sorted(grouped["algorithm"].unique()):
algo_data = grouped[grouped["algorithm"] == algo]
plt.plot(
algo_data["num_projects"],
algo_data[metric],
marker="o",
label=algo,
)

plt.title(f"{ylabel} vs. Number of Projects (voters={n_voters})")
plt.xlabel("Number of Projects")
plt.ylabel(ylabel)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
save_path = f"{RESULTS_DIR}/{metric}_voters_{n_voters}.png"
plt.savefig(save_path, dpi=150)
plt.close()
print(f"Saved {save_path}")

print("\nAll plots saved.")


if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "plot":
plot_results()
else:
run_experiments()
plot_results()
Loading
Loading