@toby-pereira Awesome! I also reached out to the original author and linked them here.
I hashed out more adjustments and a Python code chunk that runs elections with detailed audits. It's a bit sophisticated... and currently it actually removes 0-power seats rather than keeping them as a ceremonial role, probably they should still be seated in case subsequent power adjustments are computed. But the latest version and some examples are below.
The description is not very straightforward either, but the results look pretty good to me.
"""
Median-T Satiation with Dynamic Prefix Tightening + Candidate-wise RUS
Exact implementation of the specified method with detailed auditing
METHOD SPECIFICATION:
====================
1. Ballots: RAC (Rank with Approval Cutoff) - each voter ranks all candidates and approves top a_i
2. T = median(a_i) computed once at start (upper median if even)
3. Fill seats K=1 to N iteratively
4. DYNAMIC PREFIX TIGHTENING: Once satiated, a voter's active approvals are always 
   the prefix up to their CURRENT top-ranked seated winner. As better candidates 
   are seated, the prefix tightens upward. It never loosens.
5. CANDIDATE-WISE RUS: Identify specific "consensus triggers" (candidates that would
   satiate ALL remaining voters). Mark only those candidates as non-satiating, but
   allow satiation based on other winners in the set.
6. TIEBREAK PRIORITY: Prefer non-flagged candidates over RUS-flagged ones when breaking ties.
"""
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Set, Optional
import math
import pandas as pd
from collections import defaultdict
import copy
class AuditLog:
    """Detailed logging of each step in the process"""
    def __init__(self, verbose: bool = True):
        self.entries = []
        self.verbose = verbose
    
    def log(self, phase: str, rule: str, details: str, data: dict = None):
        entry = {
            "phase": phase,
            "rule": rule,
            "details": details,
            "data": data or {}
        }
        self.entries.append(entry)
        if self.verbose:
            print(f"[{phase}] {rule}")
            print(f"  → {details}")
            if data:
                for k, v in data.items():
                    print(f"    {k}: {v}")
            print()
@dataclass
class VoterGroup:
    """Represents a group of voters with identical preferences"""
    n: int                          # Number of voters in group
    rank: List[str]                 # Strict ranking of all candidates
    a: int                          # Approval cutoff (approve top a candidates)
    saturated: bool = False         # Whether group is saturated
    satiation_prefix_end: Optional[str] = None  # H: highest-ranked winner when satiated
    
    def get_prefix_candidates(self) -> Set[str]:
        """
        Get candidates in the satiation prefix (at or above H in ranking)
        """
        if not self.saturated or not self.satiation_prefix_end:
            return set(self.rank)  # All candidates if not saturated
        
        prefix = set()
        for c in self.rank:
            prefix.add(c)
            if c == self.satiation_prefix_end:
                break
        return prefix
    
    def active_approvals(self, current_winners: List[str] = None) -> Set[str]:
        """
        RULE: Active Approvals with Dynamic Prefix Tightening
        - Unsaturated: approve top a_i candidates
        - Saturated: approve only candidates in prefix up to CURRENT top-ranked winner
        """
        if not self.saturated:
            return set(self.rank[:self.a])
        
        # Saturated: dynamically compute H based on current winners
        if current_winners:
            current_H = self.top_in_set(current_winners)
            if current_H:
                # Build prefix up to current H
                prefix = set()
                for c in self.rank:
                    prefix.add(c)
                    if c == current_H:
                        break
                original_approvals = set(self.rank[:self.a])
                return original_approvals & prefix
        
        # Fallback to stored prefix
        prefix = self.get_prefix_candidates()
        original_approvals = set(self.rank[:self.a])
        return original_approvals & prefix
    
    def can_influence_h2h(self, cand_a: str, cand_b: str, current_winners: List[str] = None) -> bool:
        """
        RULE: Active Ranking Influence with Dynamic Prefix
        - Unsaturated: can influence all head-to-heads
        - Saturated: can only influence if BOTH candidates are in dynamically computed prefix
        """
        if not self.saturated:
            return True
        
        # Compute current prefix based on current winners
        if current_winners:
            current_H = self.top_in_set(current_winners)
            if current_H:
                # Build prefix up to current H
                prefix = set()
                for c in self.rank:
                    prefix.add(c)
                    if c == current_H:
                        break
                return cand_a in prefix and cand_b in prefix
        
        # Fallback to stored prefix
        prefix = self.get_prefix_candidates()
        return cand_a in prefix and cand_b in prefix
    
    def prefers(self, a: str, b: str, current_winners: List[str] = None) -> int:
        """
        Return 1 if a>b, -1 if b>a, 0 if tie
        Only valid if can_influence_h2h returns True
        """
        if not self.can_influence_h2h(a, b, current_winners):
            return 0  # No influence
        
        pos = {c: i for i, c in enumerate(self.rank)}
        ia, ib = pos.get(a, math.inf), pos.get(b, math.inf)
        if ia < ib: return 1
        if ib < ia: return -1
        return 0
    
    def top_in_set(self, winners: List[str]) -> Optional[str]:
        """
        Return highest-ranked candidate from winners (H for this voter)
        """
        for c in self.rank:
            if c in winners:
                return c
        return None
    
    def get_rank_position(self, candidate: str) -> int:
        """Get 0-based position of candidate in ranking"""
        try:
            return self.rank.index(candidate)
        except ValueError:
            return math.inf
    
    def would_satiate(self, winners: List[str], T: int) -> bool:
        """
        RULE: Satiation Test
        Voter satiates if ANY winner appears within top min(T, a_i) ranks
        """
        if self.saturated:
            return False
        
        threshold = min(T, self.a)
        for w in winners:
            pos = self.get_rank_position(w)
            if pos < threshold:
                return True
        return False
@dataclass
class Election:
    """Manages the election process with detailed auditing"""
    candidates: List[str]
    groups: List[VoterGroup]
    tie_order: List[str] = field(default_factory=list)
    audit: AuditLog = field(default_factory=AuditLog)
    
    def __post_init__(self):
        if not self.tie_order:
            self.tie_order = list(self.candidates)
        self.T = None  # Will be computed once
    
    def compute_T(self) -> int:
        """
        RULE: T Calculation
        T = median of all approval cutoffs (upper median if even)
        Computed ONCE at the start, stays fixed
        """
        if self.T is not None:
            return self.T
            
        a_list = []
        for g in self.groups:
            a_list.extend([g.a] * g.n)  # Expand by voter count
        a_list.sort()
        
        m = len(a_list)
        if m == 0:
            self.T = 0
        elif m % 2 == 1:
            self.T = a_list[m//2]
        else:
            self.T = a_list[m//2]  # Upper median for even
        
        self.audit.log(
            "INITIALIZATION", 
            "T Calculation (Median of Approval Cutoffs)",
            f"Median of {m} voters' approval cutoffs = {self.T}",
            {"all_cutoffs": a_list, "T": self.T}
        )
        return self.T
    
    def tally_active_approvals(self, current_winners: List[str] = None) -> Dict[str, int]:
        """
        RULE: Approval Tallying with Dynamic Prefix Tightening
        Count active approvals from all groups (with dynamic H adjustment for satiated voters)
        """
        tallies = {c: 0 for c in self.candidates}
        
        for i, g in enumerate(self.groups):
            approved = g.active_approvals(current_winners)
            for c in approved:
                if c in self.candidates:  # Only count if still in race
                    tallies[c] += g.n
        
        # Only return tallies for current candidates
        return {c: tallies[c] for c in self.candidates}
    
    def head_to_head(self, a: str, b: str, current_winners: List[str] = None) -> int:
        """
        RULE: Head-to-Head Tiebreaking with Dynamic Prefix
        Use rankings from voters who can influence this comparison
        """
        a_score = b_score = 0
        
        for g in self.groups:
            pref = g.prefers(a, b, current_winners)
            if pref > 0:
                a_score += g.n
            elif pref < 0:
                b_score += g.n
        
        if a_score > b_score: return 1
        if b_score > a_score: return -1
        return 0
    
    def select_provisional_winners(self, K: int, iteration: int, previous_winners: List[str] = None, 
                                   non_satiating_candidates: Set[str] = None) -> List[str]:
        """
        RULE: Pick Provisional Winners
        1. Tally active approvals (based on previous winners for dynamic prefix)
        2. Take top K by approval count
        3. Tiebreak: prioritize non-flagged over RUS-flagged, then head-to-head, then fixed order
        """
        if non_satiating_candidates is None:
            non_satiating_candidates = set()
            
        tallies = self.tally_active_approvals(previous_winners)
        
        self.audit.log(
            f"K={K} ITER-{iteration}",
            "Active Approval Tally",
            f"Current approval counts (with previous winners: {previous_winners})",
            {"tallies": tallies, "non_satiating": list(non_satiating_candidates)}
        )
        
        # Group by approval count
        by_tally = defaultdict(list)
        for c, t in tallies.items():
            by_tally[t].append(c)
        
        # Sort tallies descending
        sorted_candidates = []
        for tally in sorted(by_tally.keys(), reverse=True):
            tied = by_tally[tally]
            
            if len(tied) == 1:
                sorted_candidates.extend(tied)
            else:
                # Tiebreak needed
                self.audit.log(
                    f"K={K} ITER-{iteration}",
                    "Tiebreaking",
                    f"{len(tied)} candidates tied with {tally} approvals",
                    {"tied_candidates": tied}
                )
                
                # Sort by: (1) non-flagged before flagged, (2) head-to-head wins, (3) fixed order
                def tiebreak_key(cand):
                    is_flagged = 1 if cand in non_satiating_candidates else 0
                    wins = sum(1 for other in tied if other != cand and self.head_to_head(cand, other, previous_winners) > 0)
                    return (is_flagged, -wins, self.tie_order.index(cand))
                
                tied_sorted = sorted(tied, key=tiebreak_key)
                sorted_candidates.extend(tied_sorted)
        
        winners = sorted_candidates[:K]
        self.audit.log(
            f"K={K} ITER-{iteration}",
            "Provisional Winners Selected",
            f"Top {K} candidates by approval with tiebreaking",
            {"winners": winners}
        )
        
        return winners
    
    def apply_satiation(self, winners: List[str], T: int, non_satiating_candidates: Set[str]) -> Tuple[List[int], Set[str]]:
        """
        RULE: Median-T Satiation with Candidate-wise RUS
        Returns (list of newly satiated group indices, set of consensus triggers identified)
        """
        # First, identify consensus triggers (candidates that would satiate ALL unsaturated voters)
        # Skip candidates already flagged as non-satiating
        unsaturated_groups = [g for g in self.groups if not g.saturated]
        consensus_triggers = set()
        
        if unsaturated_groups:
            # Check each winner to see if it's a consensus trigger
            for w in winners:
                # Skip already-flagged candidates
                if w in non_satiating_candidates:
                    continue
                    
                is_consensus = True
                for g in unsaturated_groups:
                    pos = g.get_rank_position(w)
                    threshold = min(T, g.a)
                    if pos >= threshold:  # This candidate doesn't trigger this voter
                        is_consensus = False
                        break
                if is_consensus:
                    consensus_triggers.add(w)
            
            if consensus_triggers:
                self.audit.log(
                    f"K={len(winners)}",
                    "New Consensus Triggers Identified",
                    f"Candidates {consensus_triggers} would satiate ALL remaining voters",
                    {"consensus_triggers": list(consensus_triggers)}
                )
        
        # Now apply satiation, but ignore consensus triggers as satiation causes
        newly_satiated = []
        
        for gi, g in enumerate(self.groups):
            if g.saturated:
                continue
                
            # Find highest-ranked winner that's NOT a consensus trigger or already non-satiating
            ignore_for_satiation = consensus_triggers | non_satiating_candidates
            
            # Check if this voter would satiate based on non-ignored winners
            threshold = min(T, g.a)
            for w in winners:
                if w in ignore_for_satiation:
                    continue
                pos = g.get_rank_position(w)
                if pos < threshold:
                    # This voter satiates based on winner w
                    H = g.top_in_set(winners)  # Still use actual top winner for prefix
                    g.saturated = True
                    g.satiation_prefix_end = H
                    newly_satiated.append(gi)
                    
                    prefix = g.get_prefix_candidates()
                    self.audit.log(
                        f"K={len(winners)}",
                        f"Group {gi+1} Satiated",
                        f"H={H}, satiated via {w}, retaining prefix of {len(prefix)} candidates",
                        {"group_size": g.n, "H": H, "trigger": w, "prefix": list(prefix)}
                    )
                    break
        
        return newly_satiated, consensus_triggers
    
    def assign_power(self, winners: List[str]) -> Dict[str, int]:
        """
        RULE: Power Assignment
        Each voter assigns power to their highest-ranked winner
        """
        power = {w: 0 for w in winners}
        
        for g in self.groups:
            rep = g.top_in_set(winners)
            if rep:
                power[rep] += g.n
        
        return power
    
    def eliminate_zero_power(self, winners: List[str], power: Dict[str, int]) -> List[str]:
        """
        RULE: Zero-Power Elimination
        Remove winners with no voter support
        """
        eliminated = [w for w in winners if power[w] == 0]
        
        if eliminated:
            self.audit.log(
                f"K={len(winners)}",
                "Zero-Power Elimination",
                f"Removing {len(eliminated)} candidates with no support",
                {"eliminated": eliminated}
            )
            
            # Remove from candidate list
            self.candidates = [c for c in self.candidates if c not in eliminated]
            self.tie_order = [c for c in self.tie_order if c in self.candidates]
            
            # Update satiation prefixes if needed
            for g in self.groups:
                if g.satiation_prefix_end in eliminated:
                    # Find next candidate in prefix that's still valid
                    prefix_cands = []
                    for c in g.rank:
                        if c in self.candidates:
                            prefix_cands.append(c)
                        if c == g.satiation_prefix_end:
                            break
                    
                    # Update H to last valid candidate in prefix, or None
                    g.satiation_prefix_end = prefix_cands[-1] if prefix_cands else None
                    if g.satiation_prefix_end is None:
                        g.saturated = False  # No valid prefix anymore
        
        return eliminated
    
    def run_for_K(self, K: int) -> Tuple[List[str], Dict[str, int]]:
        """
        Main algorithm for selecting K winners with candidate-wise RUS and dynamic prefix tightening
        """
        self.audit.log(
            f"K={K}",
            "Starting K-Selection",
            f"Selecting {K} winners from {len(self.candidates)} candidates",
            {"candidates": self.candidates[:10] if len(self.candidates) > 10 else self.candidates}
        )
        
        T = self.compute_T()
        non_satiating_candidates = set()  # Specific candidates marked as non-satiating for this K
        
        iteration = 0
        last_winners = []  # Start with empty set
        last_power = None
        max_iterations = 100
        
        while iteration < max_iterations:
            iteration += 1
            
            # Step 1: Pick provisional winners based on previous winners (for dynamic prefix)
            # and considering non-satiating candidates for tiebreaking
            winners = self.select_provisional_winners(K, iteration, 
                                                     last_winners if iteration > 1 else None,
                                                     non_satiating_candidates)
            
            # Step 2: Apply satiation with candidate-wise RUS
            newly_satiated, found_consensus_triggers = self.apply_satiation(winners, T, non_satiating_candidates)
            
            # Compute newly added consensus triggers (not already known)
            newly_added_triggers = found_consensus_triggers - non_satiating_candidates
            
            # Add newly identified consensus triggers to non-satiating set
            if newly_added_triggers:
                non_satiating_candidates.update(newly_added_triggers)
                self.audit.log(
                    f"K={K} ITER-{iteration}",
                    "Non-satiating Candidates Updated",
                    f"Added {newly_added_triggers} to non-satiating set",
                    {"newly_added": list(newly_added_triggers), 
                     "total_non_satiating": list(non_satiating_candidates)}
                )
            
            # Step 3: Assign power
            power = self.assign_power(winners)
            self.audit.log(
                f"K={K} ITER-{iteration}",
                "Power Assignment",
                "Voter support distribution",
                {"power": power}
            )
            
            # Step 4: Eliminate zero-power winners
            eliminated = self.eliminate_zero_power(winners, power)
            
            # Check for stabilization - only count NEWLY ADDED triggers as a change
            changed = (winners != last_winners or 
                      power != last_power or 
                      newly_satiated or 
                      bool(newly_added_triggers) or  # Only newly added, not all found
                      eliminated)
            
            if not changed:
                self.audit.log(
                    f"K={K}",
                    "STABILIZED",
                    f"No changes in iteration {iteration}",
                    {"final_winners": winners, "final_power": power}
                )
                break
            
            last_winners = winners
            last_power = power
        
        return winners, power
def run_election_with_audit(title: str, candidates: List[str], groups: List[VoterGroup], 
                           max_K: int, verbose: bool = True):
    """
    Run complete election from K=1 to max_K with detailed auditing
    """
    print("="*80)
    print(f"ELECTION: {title}")
    print("="*80)
    
    if verbose:
        print("\nINITIAL CONFIGURATION:")
        print(f"Candidates: {candidates}")
        print("\nVoter Groups:")
        for i, g in enumerate(groups):
            print(f"  Group {i+1}: {g.n} voters")
            print(f"    Ranking: {' > '.join(g.rank)}")
            print(f"    Approves top {g.a}: {list(g.rank[:g.a])}")
        print()
    
    # Create fresh election (groups will carry satiation forward between K values)
    el = Election(
        candidates=list(candidates),
        groups=groups,  # These will maintain state across K
        tie_order=list(candidates),
        audit=AuditLog(verbose=verbose)
    )
    
    results = []
    for K in range(1, max_K + 1):
        if verbose:
            print(f"\n{'='*60}")
            print(f"SOLVING FOR K={K}")
            print(f"{'='*60}\n")
        
        winners, power = el.run_for_K(K)
        
        results.append({
            "K": K,
            "Winners": winners,
            "Power": power,
            "Power_str": ", ".join(f"{w}:{p}" for w, p in power.items())
        })
        
        if verbose:
            print(f"\nRESULT FOR K={K}:")
            print(f"  Winners: {winners}")
            print(f"  Power: {power}")
    
    return results
# Example scenarios
def demo_scenario():
    """Run a demonstration scenario"""
    resuls = None
    if True:
        # Scenario: Three factions with compromise candidate U
        candidates = ["A", "B", "C", "U", "D"]
        groups = [
            VoterGroup(34, ["A", "U", "B", "C", "D"], 2),
            VoterGroup(33, ["B", "U", "A", "C", "D"], 2), 
            VoterGroup(33, ["C", "U", "A", "B", "D"], 2),
        ]
        
        results = run_election_with_audit(
            "Three Factions with Universal Compromise",
            candidates,
            groups,
            max_K=3,
            verbose=True
        )
        
        # Summary table
        print("\n" + "="*80)
        print("SUMMARY")
        print("="*80)
        df = pd.DataFrame([{
            "K": r["K"],
            "Winners": ", ".join(r["Winners"]),
            "Power Distribution": r["Power_str"]
        } for r in results])
        print(df.to_string(index=False))
    elif True:
        pass
    
    return results
if __name__ == "__main__":
    demo_scenario()
# -------------------------------
# More example elections
# -------------------------------
def scenario_majority_clones_vs_two_minorities(verbose=False):
    """
    Majority (52) spreads support across 3 near-clone heads (A1,A2,A3).
    Two cohesive minorities (28 for B-first, 20 for C-first).
    Stress: clone-packing + whether minorities still seat as K grows.
    Expectation: As K increases, A-bloc locks to one/two A* winners,
    while B and C each capture representation; U (none here) not present.
    """
    candidates = ["A1","A2","A3","B","C","D"]
    groups = [
        VoterGroup(18, ["A1","A2","A3","B","C","D"], 3),
        VoterGroup(17, ["A2","A3","A1","B","C","D"], 3),
        VoterGroup(17, ["A3","A1","A2","B","C","D"], 3),
        VoterGroup(28, ["B","C","A1","A2","A3","D"], 2),
        VoterGroup(20, ["C","B","A1","A2","A3","D"], 2),
    ]
    return run_election_with_audit(
        "Majority Clones vs Two Minorities",
        candidates, groups, max_K=4, verbose=verbose
    )
def scenario_heterogeneous_T(verbose=False):
    """
    Heterogeneous approval cutoffs so median-T matters.
    30 voters approve only top-1; 30 approve top-2; 40 approve top-3.
    Expectation: T=2 (upper median). Compromise M is high but not always seated
    unless supported by multiple blocs; dynamic tightening should peel off approvals.
    """
    candidates = ["A","B","C","M","D","E"]
    groups = [
        VoterGroup(30, ["A","M","B","C","D","E"], 1),
        VoterGroup(30, ["B","M","A","C","D","E"], 2),
        VoterGroup(40, ["C","M","B","A","D","E"], 3),
    ]
    return run_election_with_audit(
        "Heterogeneous T (1/2/3) with Compromise M",
        candidates, groups, max_K=3, verbose=verbose
    )
def scenario_big_majority_plus_consensus_U(verbose=False):
    """
    Big majority (60) vs minority (40), with widely approved compromise U.
    Approvals: Majority approves {A, U}; Minority approves {B, U}.
    Expectation: K=1 likely U; as K grows, blocs lock to A and B and U falls back.
    """
    candidates = ["A","B","U","C","D"]
    groups = [
        VoterGroup(60, ["A","U","B","C","D"], 2),
        VoterGroup(40, ["B","U","A","C","D"], 2),
    ]
    return run_election_with_audit(
        "Big Majority vs Minority with Consensus U",
        candidates, groups, max_K=3, verbose=verbose
    )
def scenario_two_parties_plus_centrist(verbose=False):
    """
    Two parties (45/45) with distinct heads (A,B) and a centrist M broadly approved.
    Small 10-voter group prefers a reformer R (also approves M).
    Expectation: K=1 often M; K=2/3 should seat A and B; R may or may not make it at K=3/4.
    """
    candidates = ["A","B","M","R","D"]
    groups = [
        VoterGroup(45, ["A","M","B","R","D"], 2),
        VoterGroup(45, ["B","M","A","R","D"], 2),
        VoterGroup(10, ["R","M","A","B","D"], 2),
    ]
    return run_election_with_audit(
        "Two Parties + Centrist + Reformer",
        candidates, groups, max_K=4, verbose=verbose
    )
def scenario_clone_sprinkling_attempt(verbose=False):
    """
    Simulate a 'decoy sprinkling' attempt: the 55-voter bloc splits into
    three sub-blocs each ranking a different decoy D* first, all approving their real champion X as well.
    Minority prefers Y and Z. Tests candidate-wise RUS + tightening against seat-packing.
    """
    candidates = ["X","Y","Z","D1","D2","D3","W"]
    groups = [
        VoterGroup(19, ["D1","X","D2","D3","Y","Z","W"], 2),
        VoterGroup(18, ["D2","X","D3","D1","Y","Z","W"], 2),
        VoterGroup(18, ["D3","X","D1","D2","Y","Z","W"], 2),
        VoterGroup(25, ["Y","Z","X","D1","D2","D3","W"], 2),
        VoterGroup(20, ["Z","Y","X","D1","D2","D3","W"], 2),
    ]
    return run_election_with_audit(
        "Clone Sprinkling Attempt (Decoys D1–D3)",
        candidates, groups, max_K=4, verbose=verbose
    )
def scenario_many_seats_party_listish(verbose=False):
    """
    Party-list-ish: A:40, B:35, C:25 with some cross-approval for a shared governance U.
    Test proportionality as K increases (K up to 5).
    """
    candidates = ["A1","A2","B1","B2","C1","U","D"]
    groups = [
        VoterGroup(40, ["A1","U","A2","B1","C1","B2","D"], 2),
        VoterGroup(35, ["B1","U","B2","A1","C1","A2","D"], 2),
        VoterGroup(25, ["C1","U","A1","B1","A2","B2","D"], 2),
    ]
    return run_election_with_audit(
        "Many Seats, Party-list-ish with Shared U",
        candidates, groups, max_K=5, verbose=verbose
    )
def scenario_tie_sensitivity(verbose=False):
    """
    Tight ties across factions; tie order matters.
    Use symmetric 33/33/34 with shared approvals to force frequent ties.
    Verify your tie-break ('unflagged before flagged', then H2H, then fixed).
    """
    candidates = ["A","B","C","M","N"]
    groups = [
        VoterGroup(34, ["A","M","B","C","N"], 2),
        VoterGroup(33, ["B","M","C","A","N"], 2),
        VoterGroup(33, ["C","M","A","B","N"], 2),
    ]
    return run_election_with_audit(
        "Tie Sensitivity (Symmetric 34/33/33)",
        candidates, groups, max_K=3, verbose=verbose
    )
def scenario_median_T_equals_one(verbose=False):
    """
    Force T=1: simple-majority may try to set median approval to 1.
    Everyone approves exactly one (a_i=1), different heads.
    Expectation: behaves close to STV-ish seat allocation by top ranks; 
    dynamic tightening is trivial but RUS can still mute universal names.
    """
    candidates = ["A","B","C","U","D"]
    groups = [
        VoterGroup(40, ["A","U","B","C","D"], 1),
        VoterGroup(35, ["B","U","A","C","D"], 1),
        VoterGroup(25, ["C","U","A","B","D"], 1),
    ]
    return run_election_with_audit(
        "Median T = 1 (All a_i=1)",
        candidates, groups, max_K=3, verbose=verbose
    )
def run_more_examples(verbose=False):
    """
    Run all the example scenarios above.
    Set verbose=True for full step-by-step audits.
    """
    all_results = []
    print("\n" + "="*80)
    print("SCENARIO 1: Majority Clones vs Two Minorities")
    print("="*80)
    all_results.append(scenario_majority_clones_vs_two_minorities(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 2: Heterogeneous T (1/2/3) with Compromise M")
    print("="*80)
    all_results.append(scenario_heterogeneous_T(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 3: Big Majority vs Minority with Consensus U")
    print("="*80)
    all_results.append(scenario_big_majority_plus_consensus_U(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 4: Two Parties + Centrist + Reformer")
    print("="*80)
    all_results.append(scenario_two_parties_plus_centrist(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 5: Clone Sprinkling Attempt (Decoys D1–D3)")
    print("="*80)
    all_results.append(scenario_clone_sprinkling_attempt(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 6: Many Seats, Party-list-ish with Shared U")
    print("="*80)
    all_results.append(scenario_many_seats_party_listish(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 7: Tie Sensitivity (Symmetric 34/33/33)")
    print("="*80)
    all_results.append(scenario_tie_sensitivity(verbose=verbose))
    print("\n" + "="*80)
    print("SCENARIO 8: Median T = 1 (All a_i=1)")
    print("="*80)
    all_results.append(scenario_median_T_equals_one(verbose=verbose))
    # Print compact summaries for each scenario’s last run
    print("\n" + "="*80)
    print("SUMMARY (compact)")
    print("="*80)
    for bundle in all_results:
        # bundle is the list returned by run_election_with_audit (one dict per K)
        df = pd.DataFrame([{
            "K": r["K"],
            "Winners": ", ".join(r["Winners"]),
            "Power": r["Power_str"]
        } for r in bundle])
        print(df.to_string(index=False))
        print("-"*80)
    return all_results