{"version":3,"sources":["../../../src/election/attribution/proportionalBase.ts","../../../src/election/attribution/metrics.ts"],"sourcesContent":["import { Counter } from \"@gouvernathor/python/collections\";\nimport { type Simple } from \"../ballots\";\nimport { type Attribution, type HasNSeats } from \"../attribution\";\nimport { defaultMetric, type DisproportionMetric } from \"./metrics\";\n\n/**\n * An attribution method that allocates seats proportionally\n * to the number of votes received by each party.\n */\nexport interface Proportional<Party> extends Attribution<Party, Simple<Party>> {};\n\n// TODO: doesn't it always implement HasNSeats ?\n/**\n * An specific kind of proportional attribution method,\n * based on a rank-index function, and which\n */\nexport interface RankIndexMethod<Party> extends Proportional<Party> {};\n\n/**\n * A function that should be pure : it should not take into account\n * any value other than the arguments passed to it.\n *\n * @param t The fraction of votes received by a candidate\n * @param a The number of seats already allocated to that candidate\n * @returns A value that should be increasing as t rises, and decreasing as a rises.\n * The higher the return value, the more likely the candidate is to receive another seat.\n */\nexport interface RankIndexFunction {\n    (t: number, a: number): number;\n};\n\n/**\n * A function creating a fixed-seats, proportional, rank-index attribution method\n * from a rank-index function.\n *\n * The implementation is optimized so as to call rankIndexFunction as few times as possible.\n *\n * Replaces the RankIndexMethod class implementation.\n */\nexport function proportionalFromRankIndexFunction<Party>(\n    { nSeats, rankIndexFunction }: {\n        nSeats: number,\n        rankIndexFunction: RankIndexFunction,\n    }\n): RankIndexMethod<Party> & HasNSeats {\n    const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n        const allVotes = votes.total;\n        const fractions = new Map([...votes.entries()].map(([party, v]) => [party, v / allVotes]));\n\n        const rankIndexValues = new Map([...fractions.entries()].map(([party, f]) => [party, rankIndexFunction(f, 0)]));\n\n        // the parties, sorted by increasing rankIndex value\n        const parties = [...votes.keys()].sort((a, b) => rankIndexValues.get(a)! - rankIndexValues.get(b)!);\n\n        const seats = new Counter<Party>();\n\n        s: for (let sn = 0; sn < nSeats; sn++) {\n            // take the most deserving party\n            const winner = parties.pop()!;\n            // give it a seat\n            seats.increment(winner);\n            // update the rankIndex value of the party\n            rankIndexValues.set(winner, rankIndexFunction(fractions.get(winner)!, seats.get(winner)!));\n\n            // insert it in its (new) place in the sorted list of parties\n            for (let pn = 0; pn < parties.length; pn++) {\n                if (rankIndexValues.get(parties[pn])! >= rankIndexValues.get(winner)!) {\n                    parties.splice(pn, 0, winner);\n                    continue s;\n                }\n            }\n            parties.push(winner);\n        }\n\n        return seats;\n    };\n    attrib.nSeats = nSeats;\n    return attrib;\n}\n\n/**\n * Creates a rank-index (proportional) attribution method in which\n * the total number of seats is NOT fixed.\n * Instead, it takes a metric of disproportionality\n * between the entitlements (the votes) and the numbers of seats,\n * and a range of valid number of seats,\n * and the attribution method will return the allocation of seats\n * that minimizes the metric, while respecting the range of valid number of seats.\n *\n * The attribution will only return a 0-seats attribution when the maxNSeats is 0,\n * otherwise, a minNSeats value of 0 will be treated as 1.\n *\n * The metric has a reasonable default.\n *\n * The implementation is still optimized so as to call rankIndexFunction as few times as possible.\n *\n * @param minNSeats The minimum number of seats to be allocated, inclusive.\n * @param maxNSeats The maximum number of seats to be allocated, inclusive.\n */\nexport function boundedRankIndexMethod<Party>(\n    { minNSeats, maxNSeats, rankIndexFunction, metric = defaultMetric }: {\n        minNSeats: number,\n        maxNSeats: number,\n        rankIndexFunction: RankIndexFunction,\n        metric?: DisproportionMetric<Party>,\n    }\n): RankIndexMethod<Party> {\n    const attrib = (votes: Simple<Party>, rest = {}): Counter<Party> => {\n        const allVotes = votes.total;\n        const fractions = new Map([...votes.entries()].map(([party, v]) => [party, v / allVotes]));\n\n        const rankIndexValues = new Map([...fractions.entries()].map(([party, f]) => [party, rankIndexFunction(f, 0)]));\n\n        // the parties, sorted by increasing rankIndex value\n        const parties = [...votes.keys()].sort((a, b) => rankIndexValues.get(a)! - rankIndexValues.get(b)!);\n\n        const seats = new Counter<Party>();\n\n        let bestSeats = seats.pos;\n        // technically, most metrics give 0 for a 0-seats attribution\n        // but we have to patch that out otherwise the 0-seats attribution will always be returned\n        let bestSeatsMetric = Infinity;\n\n        s: for (let sn = 1; sn <= maxNSeats; sn++) {\n            // take the most deserving party\n            const winner = parties.pop()!;\n            // give it a seat\n            seats.increment(winner);\n\n            if (sn >= minNSeats) {\n                // compute the metric of the new attribution\n                const newMetric = metric({ votes, seats });\n                if (newMetric < bestSeatsMetric) { // when above the min, favor fewer seats\n                    bestSeats = seats.pos;\n                    bestSeatsMetric = newMetric;\n                }\n            }\n\n            // update the rankIndex value of the party\n            rankIndexValues.set(winner, rankIndexFunction(fractions.get(winner)!, seats.get(winner)!));\n\n            // insert it in its (new) place in the sorted list of parties\n            for (let pn = 0; pn < parties.length; pn++) {\n                if (rankIndexValues.get(parties[pn])! >= rankIndexValues.get(winner)!) {\n                    parties.splice(pn, 0, winner);\n                    continue s;\n                }\n            }\n            parties.push(winner);\n        }\n\n        return bestSeats;\n    }\n    attrib.minNSeats = minNSeats;\n    attrib.maxNSeats = maxNSeats;\n    return attrib;\n}\n\nexport interface DivisorMethod<Party> extends RankIndexMethod<Party> {}\n\n/**\n * A function that should be pure.\n * @param k The number of seats already allocated to a party\n * @returns A value that should be increasing as k rises\n */\nexport interface DivisorFunction {\n    (k: number): number;\n}\n\nexport function stationaryDivisorFunction(r: number): DivisorFunction {\n    return (k: number) => k + r;\n}\n\nexport function rankIndexFunctionFromDivisorFunction(\n    divisorFunction: DivisorFunction\n): RankIndexFunction {\n    return (t, a) => t / divisorFunction(a);\n}\n\n/**\n * A function creating a divisor method -\n * one kind of rank-index attribution, itself a kind of proportional attribution.\n */\nexport function proportionalFromDivisorFunction<Party>(\n    { nSeats, divisorFunction }: {\n        nSeats: number,\n        divisorFunction: DivisorFunction,\n    }\n): DivisorMethod<Party> & HasNSeats {\n    return proportionalFromRankIndexFunction({\n        nSeats,\n        rankIndexFunction: rankIndexFunctionFromDivisorFunction(divisorFunction),\n    });\n}\n","import { type Counter } from \"@gouvernathor/python/collections\";\nimport { type Simple } from \"../ballots\";\n\nexport interface DisproportionMetric<Party> {\n    (p0: {votes: Simple<Party>, seats: Counter<Party>}): number;\n}\n\n/**\n * Returns the mean value (across all candidates) of the absolute difference\n * between the theoretical, (f)ra(c)tional number of seats and the allocated number of seats.\n * The bigger, the less proportional.\n *\n * Compared to a sum or mean of absolute differences of percentage,\n * this\n */\nexport function defaultMetric<Party>(\n    { votes, seats }: {\n        votes: Simple<Party>,\n        seats: Counter<Party>,\n    }\n): number {\n    const allVotes = votes.total;\n    const allSeats = seats.total;\n    let suum = 0;\n    for (const party of seats.keys()) {\n        const partyVotes = votes.get(party)!;\n        const partySeats = seats.get(party)!;\n        suum += Math.abs(allSeats * partyVotes / allVotes - partySeats);\n    }\n    return suum / seats.size;\n}\n"],"mappings":"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","names":[]}