equalshares 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/exe/equalshares +7 -0
- data/lib/equalshares/cli/formatter.rb +79 -0
- data/lib/equalshares/cli.rb +100 -0
- data/lib/equalshares/comparison.rb +48 -0
- data/lib/equalshares/completion.rb +106 -0
- data/lib/equalshares/compute.rb +19 -0
- data/lib/equalshares/election.rb +77 -0
- data/lib/equalshares/errors.rb +11 -0
- data/lib/equalshares/fixed_budget.rb +242 -0
- data/lib/equalshares/greedy.rb +12 -0
- data/lib/equalshares/instance.rb +60 -0
- data/lib/equalshares/max_flow.rb +89 -0
- data/lib/equalshares/maximin.rb +12 -0
- data/lib/equalshares/mes_general.rb +13 -0
- data/lib/equalshares/pabulib/csv.rb +56 -0
- data/lib/equalshares/pabulib/parser.rb +85 -0
- data/lib/equalshares/pabulib/project_row_parser.rb +60 -0
- data/lib/equalshares/pabulib/vote_row_parser.rb +96 -0
- data/lib/equalshares/pabulib/writer.rb +44 -0
- data/lib/equalshares/pabulib.rb +43 -0
- data/lib/equalshares/params.rb +61 -0
- data/lib/equalshares/phragmen.rb +12 -0
- data/lib/equalshares/result.rb +47 -0
- data/lib/equalshares/rules/base.rb +44 -0
- data/lib/equalshares/rules/cardinal_mes.rb +98 -0
- data/lib/equalshares/rules/greedy.rb +49 -0
- data/lib/equalshares/rules/maximin.rb +119 -0
- data/lib/equalshares/rules/method_of_equal_shares.rb +80 -0
- data/lib/equalshares/rules/phragmen.rb +68 -0
- data/lib/equalshares/satisfaction.rb +39 -0
- data/lib/equalshares/statistics.rb +34 -0
- data/lib/equalshares/tie_breaker.rb +95 -0
- data/lib/equalshares/tie_breaking.rb +48 -0
- data/lib/equalshares/version.rb +5 -0
- data/lib/equalshares.rb +40 -0
- metadata +86 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Equalshares
|
|
4
|
+
module Rules
|
|
5
|
+
# The Method of Equal Shares for approval ballots — the equalshares.net rule,
|
|
6
|
+
# including its completion methods (none/utilitarian/add1/...) and comparison step.
|
|
7
|
+
# Faithful port of equalShares() in js/methodOfEqualSharesWorker.js.
|
|
8
|
+
#
|
|
9
|
+
# Cost and budget are treated as floats at this level (statistics, comparison,
|
|
10
|
+
# utilitarian/greedy committees, the everything-affordable check); only the inner
|
|
11
|
+
# fixed-budget MES loop switches to exact rationals for accuracy "fractions".
|
|
12
|
+
class MethodOfEqualShares < Base
|
|
13
|
+
ADD1_COMPLETIONS = %w[add1 add1e add1u add1eu].freeze
|
|
14
|
+
UTILITARIAN_COMPLETIONS = %w[utilitarian add1u].freeze
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
start = now
|
|
18
|
+
voter_ids = election.voter_ids
|
|
19
|
+
project_ids = election.project_ids
|
|
20
|
+
approvers = election.approvers
|
|
21
|
+
|
|
22
|
+
# Strings for the MES loop (it wraps them per accuracy); floats for the
|
|
23
|
+
# completion/comparison/statistics bookkeeping (as the JS does).
|
|
24
|
+
cost_source = project_ids.to_h { |c| [c, instance.projects[c]["cost"]] }
|
|
25
|
+
cost = election.float_costs
|
|
26
|
+
b_float = election.float_budget
|
|
27
|
+
|
|
28
|
+
mes = run_mes(cost_source, instance.budget)
|
|
29
|
+
winners = mes.fetch(:winners)
|
|
30
|
+
report = mes.fetch(:report)
|
|
31
|
+
notes = {
|
|
32
|
+
endowment: report[:endowment],
|
|
33
|
+
money_behind_candidate: report[:money_behind_candidate],
|
|
34
|
+
effective_vote_count: report[:effective_vote_count]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# utilitarian completion if needed
|
|
38
|
+
if UTILITARIAN_COMPLETIONS.include?(params.completion)
|
|
39
|
+
completion = Completion.utilitarian(voter_ids, project_ids, cost, approvers, b_float, winners)
|
|
40
|
+
winners = completion.fetch(:winners)
|
|
41
|
+
notes[:added_by_utilitarian_completion] = completion.fetch(:added)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# comparison step
|
|
45
|
+
greedy = Completion.utilitarian(voter_ids, project_ids, cost, approvers, b_float, []).fetch(:winners)
|
|
46
|
+
unless params.comparison == "none"
|
|
47
|
+
cmp = Comparison.comparison_step(voter_ids, approvers, greedy, winners, params)
|
|
48
|
+
unless cmp[:stick_to_mes]
|
|
49
|
+
winners = greedy
|
|
50
|
+
notes[:comparison] =
|
|
51
|
+
"The committee chosen by the greedy algorithm is preferred by #{cmp[:prefers_greedy]} voters, " \
|
|
52
|
+
"while the committee chosen by the method of equal shares is preferred by #{cmp[:prefers_mes]} voters."
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
notes[:greedy_stats] = election.statistics(greedy)
|
|
57
|
+
result(winners, start, notes)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def run_mes(cost_source, budget_source)
|
|
63
|
+
voter_ids = election.voter_ids
|
|
64
|
+
project_ids = election.project_ids
|
|
65
|
+
approvers = election.approvers
|
|
66
|
+
everything_affordable = election.float_costs.values.sum <= election.float_budget
|
|
67
|
+
|
|
68
|
+
if %w[none utilitarian].include?(params.completion) || everything_affordable
|
|
69
|
+
# don't use Add1 if everything is affordable
|
|
70
|
+
FixedBudget.run(voter_ids, project_ids, cost_source, approvers, budget_source, params,
|
|
71
|
+
report_details: true, progress: progress)
|
|
72
|
+
elsif ADD1_COMPLETIONS.include?(params.completion)
|
|
73
|
+
Completion.add1(voter_ids, project_ids, cost_source, approvers, budget_source, params, progress: progress)
|
|
74
|
+
else
|
|
75
|
+
raise ComputeError, "Unknown completion rule: #{params.completion}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Equalshares
|
|
4
|
+
module Rules
|
|
5
|
+
# Phragmén's sequential rule for approval participatory budgeting. Faithful port of
|
|
6
|
+
# pabutools' sequential_phragmen (resolute case).
|
|
7
|
+
#
|
|
8
|
+
# Voters accumulate "load" (starting at 0). Buying a project raises all its
|
|
9
|
+
# approvers to a common load level (sum of their loads + cost) / |approvers|. At
|
|
10
|
+
# each step the project minimising that new maximum load is bought; the rule stops
|
|
11
|
+
# once the next project to buy would exceed the budget.
|
|
12
|
+
class Phragmen < Base
|
|
13
|
+
def call
|
|
14
|
+
start = now
|
|
15
|
+
project_ids = election.project_ids
|
|
16
|
+
approvers = election.approvers
|
|
17
|
+
cost = election.costs
|
|
18
|
+
budget_limit = election.budget
|
|
19
|
+
approval_score = project_ids.to_h { |c| [c, approvers[c].length] }
|
|
20
|
+
|
|
21
|
+
loads = Hash.new(0) # voter id -> current load
|
|
22
|
+
remaining = project_ids.select { |c| cost[c] <= budget_limit }
|
|
23
|
+
winners = []
|
|
24
|
+
current_cost = 0
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
break if remaining.empty?
|
|
28
|
+
|
|
29
|
+
min_maxload, argmin = min_new_maxload(remaining, approvers, loads, cost, approval_score)
|
|
30
|
+
|
|
31
|
+
# Stop as soon as the next project to be bought would violate the budget.
|
|
32
|
+
break if argmin.any? { |c| current_cost + cost[c] > budget_limit }
|
|
33
|
+
|
|
34
|
+
selected = Tie.resolve_one(project_ids, cost, approvers, params, argmin)
|
|
35
|
+
approvers[selected].each { |i| loads[i] = min_maxload }
|
|
36
|
+
winners << selected
|
|
37
|
+
current_cost += cost[selected]
|
|
38
|
+
remaining.delete(selected)
|
|
39
|
+
progress&.call((100 * current_cost / budget_limit).floor)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
result(winners, start)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def min_new_maxload(remaining, approvers, loads, cost, approval_score)
|
|
48
|
+
min_maxload = nil
|
|
49
|
+
argmin = []
|
|
50
|
+
remaining.each do |c|
|
|
51
|
+
new_maxload =
|
|
52
|
+
if approval_score[c].zero?
|
|
53
|
+
Float::INFINITY
|
|
54
|
+
else
|
|
55
|
+
(approvers[c].sum { |i| loads[i] } + cost[c]) / approval_score[c]
|
|
56
|
+
end
|
|
57
|
+
if min_maxload.nil? || new_maxload < min_maxload
|
|
58
|
+
min_maxload = new_maxload
|
|
59
|
+
argmin = [c]
|
|
60
|
+
elsif new_maxload == min_maxload
|
|
61
|
+
argmin << c
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
[min_maxload, argmin]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Equalshares
|
|
4
|
+
# Additive satisfaction measures for approval ballots, matching pabutools.
|
|
5
|
+
#
|
|
6
|
+
# For the Method of Equal Shares, only the per-approver marginal utility u_i(c)
|
|
7
|
+
# of a project matters, and for these measures it is uniform across all approvers
|
|
8
|
+
# of c (it depends on c, not on i). This lets the fixed-budget loop stay unchanged
|
|
9
|
+
# in its payment mechanics (each project still has to raise its full cost, split as
|
|
10
|
+
# equally as possible) while the *selection* criterion becomes u(c) / maxPayment.
|
|
11
|
+
#
|
|
12
|
+
# Cost_Sat u(c) = cost(c) (equalshares.net default)
|
|
13
|
+
# Cardinality_Sat u(c) = 1
|
|
14
|
+
#
|
|
15
|
+
# Both are verified to match pabutools exactly (Cost_Sat and Cardinality_Sat).
|
|
16
|
+
# Each strategy returns the per-approver utility in the same numeric family as the
|
|
17
|
+
# cost passed in (Rational for exact mode, Float for float mode), so downstream
|
|
18
|
+
# arithmetic keeps its accuracy.
|
|
19
|
+
module Satisfaction
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
NAMES = %w[cost cardinality effort].freeze
|
|
23
|
+
|
|
24
|
+
def for(name)
|
|
25
|
+
case name
|
|
26
|
+
when "cost" then COST
|
|
27
|
+
when "cardinality" then CARDINALITY
|
|
28
|
+
when "effort" then EFFORT
|
|
29
|
+
else
|
|
30
|
+
raise ComputeError, "Unknown satisfaction measure: #{name} (allowed: #{NAMES.join(', ')})"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Each measure is a callable per_voter(cost_c, num_approvers) -> Numeric.
|
|
35
|
+
COST = ->(cost_c, _n) { cost_c }
|
|
36
|
+
CARDINALITY = ->(_cost_c, _n) { 1 }
|
|
37
|
+
EFFORT = ->(cost_c, n) { cost_c / n }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Equalshares
|
|
4
|
+
# Outcome statistics. Faithful port of gatherOutcomeStatistics() in
|
|
5
|
+
# js/methodOfEqualSharesWorker.js.
|
|
6
|
+
module Statistics
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# `cost` is the numeric cost map (Float or Rational).
|
|
10
|
+
def gather(voter_ids, cost, approvers, winners)
|
|
11
|
+
n = voter_ids.length
|
|
12
|
+
total_cost = winners.sum { |c| cost[c] }
|
|
13
|
+
avg_approved_projects = winners.sum { |c| approvers[c].length }.to_f / n
|
|
14
|
+
avg_cost_of_winning_approved = winners.sum { |c| approvers[c].length * cost[c] }.to_f / n
|
|
15
|
+
|
|
16
|
+
voter_utility = Hash.new(0)
|
|
17
|
+
winners.each do |c|
|
|
18
|
+
approvers[c].each { |i| voter_utility[i] += 1 }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# For each r, how many voters approve exactly r winning projects?
|
|
22
|
+
utility_distribution = {}
|
|
23
|
+
(0..winners.length).each { |util| utility_distribution[util] = 0 }
|
|
24
|
+
voter_ids.each { |i| utility_distribution[voter_utility[i]] += 1 }
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
total_cost: total_cost,
|
|
28
|
+
avg_approved_projects: avg_approved_projects,
|
|
29
|
+
avg_cost_of_winning_approved_projects: avg_cost_of_winning_approved,
|
|
30
|
+
utility_distribution: utility_distribution
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Equalshares
|
|
4
|
+
# Tie-breaking strategies. Each element of params.tie_breaking maps to one strategy
|
|
5
|
+
# object (via TieBreaker.for). A strategy can either narrow a tied set (#filter, used
|
|
6
|
+
# to resolve a single tie in priority order) or contribute a sort key (#sort_key, used
|
|
7
|
+
# to build a total order over all projects). Mirrors pabutools' TieBreakingRule.
|
|
8
|
+
module TieBreaker
|
|
9
|
+
# Cost/approver lookups a strategy needs. `cost` is the rule's numeric cost map.
|
|
10
|
+
Context = Struct.new(:cost, :approvers)
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def for(method)
|
|
15
|
+
case method
|
|
16
|
+
when "maxVotes" then MaxVotes.new
|
|
17
|
+
when "minCost" then MinCost.new
|
|
18
|
+
when "maxCost" then MaxCost.new
|
|
19
|
+
when Hash then Lexico.new(lexico_order(method))
|
|
20
|
+
when Array then ExplicitList.new(method)
|
|
21
|
+
else
|
|
22
|
+
raise ComputeError, "Unknown tie-breaking method: #{method}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lexico_order(method)
|
|
27
|
+
order = method[:lexico] || method["lexico"]
|
|
28
|
+
raise ComputeError, "Unknown tie-breaking method: #{method}" unless order.is_a?(Array)
|
|
29
|
+
|
|
30
|
+
order
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Keep the projects tied at the best value of some key.
|
|
34
|
+
class Extreme
|
|
35
|
+
def filter(remaining, ctx)
|
|
36
|
+
best = remaining.map { |c| key(c, ctx) }.public_send(extreme)
|
|
37
|
+
remaining.select { |c| key(c, ctx) == best }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class MaxVotes < Extreme
|
|
42
|
+
def key(project_id, ctx) = ctx.approvers[project_id].length
|
|
43
|
+
def extreme = :max
|
|
44
|
+
def sort_key(project_id, ctx) = -ctx.approvers[project_id].length
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class MinCost < Extreme
|
|
48
|
+
def key(project_id, ctx) = ctx.cost[project_id]
|
|
49
|
+
def extreme = :min
|
|
50
|
+
def sort_key(project_id, ctx) = ctx.cost[project_id]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class MaxCost < Extreme
|
|
54
|
+
def key(project_id, ctx) = ctx.cost[project_id]
|
|
55
|
+
def extreme = :max
|
|
56
|
+
def sort_key(project_id, ctx) = -ctx.cost[project_id]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# A total order (pabutools-style): rank tied projects by their position in the list
|
|
60
|
+
# and keep the first.
|
|
61
|
+
class Lexico
|
|
62
|
+
def initialize(order)
|
|
63
|
+
@order = order
|
|
64
|
+
@position = order.each_with_index.to_h
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def filter(remaining, _ctx)
|
|
68
|
+
[remaining.min_by { |c| @position[c] || @order.length }]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def sort_key(project_id, _ctx)
|
|
72
|
+
@position[project_id] || @order.length
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# JS-faithful explicit list: keep the first *choice* that appears anywhere in the
|
|
77
|
+
# list (does NOT respect the list's ordering). Kept for bit-compatibility with the
|
|
78
|
+
# equalshares.net tool; prefer Lexico for an order-respecting tie-break.
|
|
79
|
+
class ExplicitList
|
|
80
|
+
def initialize(list)
|
|
81
|
+
@list = list
|
|
82
|
+
@position = list.each_with_index.to_h
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def filter(remaining, _ctx)
|
|
86
|
+
found = remaining.find { |c| @list.include?(c) }
|
|
87
|
+
found ? [found] : remaining
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def sort_key(project_id, _ctx)
|
|
91
|
+
@position[project_id] || @list.length
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Equalshares
|
|
4
|
+
# Tie-breaking facade. Composes the TieBreaker strategies named in params.tie_breaking
|
|
5
|
+
# to either resolve a single tie (break_ties) or build a total order (total_order).
|
|
6
|
+
module Tie
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Resolve a tie among `choices` by applying each tie-breaking strategy in priority
|
|
10
|
+
# order, narrowing the set. Returns the surviving candidates (callers treat more
|
|
11
|
+
# than one as an unresolved tie).
|
|
12
|
+
def break_ties(_project_ids, cost, approvers, params, choices)
|
|
13
|
+
ctx = TieBreaker::Context.new(cost, approvers)
|
|
14
|
+
remaining = params.tie_breaking.reduce(choices.dup) do |current, method|
|
|
15
|
+
TieBreaker.for(method).filter(current, ctx)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
raise ComputeError, "Tie-breaking failed in a way that should not happen: #{choices}" if remaining.empty?
|
|
19
|
+
|
|
20
|
+
remaining
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Resolve a tie down to a single winner, raising if the tie-breaking rules leave
|
|
24
|
+
# more than one candidate. Used by the sequential rules.
|
|
25
|
+
def resolve_one(project_ids, cost, approvers, params, choices)
|
|
26
|
+
resolved = break_ties(project_ids, cost, approvers, params, choices)
|
|
27
|
+
if resolved.length > 1
|
|
28
|
+
raise ComputeError,
|
|
29
|
+
"Tie-breaking failed: tie between projects #{resolved.join(', ')} could not be resolved. " \
|
|
30
|
+
"Another tie-breaking needs to be added."
|
|
31
|
+
end
|
|
32
|
+
resolved[0]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A total order of all projects induced by params.tie_breaking, used by rules that
|
|
36
|
+
# need a full ordering (e.g. greedy welfare). Projects are sorted by each strategy's
|
|
37
|
+
# sort key in priority order, falling back to their original (JS Object.keys) order.
|
|
38
|
+
def total_order(project_ids, cost, approvers, params)
|
|
39
|
+
ctx = TieBreaker::Context.new(cost, approvers)
|
|
40
|
+
strategies = params.tie_breaking.map { |method| TieBreaker.for(method) }
|
|
41
|
+
base_index = project_ids.each_with_index.to_h
|
|
42
|
+
|
|
43
|
+
project_ids.sort_by do |c|
|
|
44
|
+
strategies.map { |strategy| strategy.sort_key(c, ctx) } + [base_index[c]]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/equalshares.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "equalshares/version"
|
|
4
|
+
require_relative "equalshares/errors"
|
|
5
|
+
require_relative "equalshares/instance"
|
|
6
|
+
require_relative "equalshares/satisfaction"
|
|
7
|
+
require_relative "equalshares/params"
|
|
8
|
+
require_relative "equalshares/pabulib"
|
|
9
|
+
require_relative "equalshares/tie_breaker"
|
|
10
|
+
require_relative "equalshares/tie_breaking"
|
|
11
|
+
require_relative "equalshares/statistics"
|
|
12
|
+
require_relative "equalshares/election"
|
|
13
|
+
require_relative "equalshares/fixed_budget"
|
|
14
|
+
require_relative "equalshares/completion"
|
|
15
|
+
require_relative "equalshares/comparison"
|
|
16
|
+
require_relative "equalshares/max_flow"
|
|
17
|
+
require_relative "equalshares/result"
|
|
18
|
+
|
|
19
|
+
# Rule objects
|
|
20
|
+
require_relative "equalshares/rules/base"
|
|
21
|
+
require_relative "equalshares/rules/method_of_equal_shares"
|
|
22
|
+
require_relative "equalshares/rules/cardinal_mes"
|
|
23
|
+
require_relative "equalshares/rules/phragmen"
|
|
24
|
+
require_relative "equalshares/rules/greedy"
|
|
25
|
+
require_relative "equalshares/rules/maximin"
|
|
26
|
+
|
|
27
|
+
# Facades over the rule objects (stable public API)
|
|
28
|
+
require_relative "equalshares/phragmen"
|
|
29
|
+
require_relative "equalshares/greedy"
|
|
30
|
+
require_relative "equalshares/maximin"
|
|
31
|
+
require_relative "equalshares/mes_general"
|
|
32
|
+
require_relative "equalshares/compute"
|
|
33
|
+
|
|
34
|
+
module Equalshares
|
|
35
|
+
# Convenience: parse a .pb file and compute the outcome in one call.
|
|
36
|
+
def self.compute_file(path, params = Params.new, progress: nil)
|
|
37
|
+
instance = Pabulib.parse_file(path)
|
|
38
|
+
Compute.equal_shares(instance, params, progress: progress)
|
|
39
|
+
end
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: equalshares
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- takahashim
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A Ruby implementation of the equalshares.net compute tool. Parses pabulib
|
|
13
|
+
(.pb) files and computes winning projects using the Method of Equal Shares (with
|
|
14
|
+
tie-breaking, Add1/utilitarian completion and a comparison step), plus Phragmén's
|
|
15
|
+
sequential rule, greedy utilitarian welfare and maximin support. Supports approval,
|
|
16
|
+
cardinal (scoring/cumulative) and ordinal ballots, several satisfaction measures,
|
|
17
|
+
and both exact (Rational) and floating-point arithmetic. Cross-checked against pabutools.
|
|
18
|
+
email:
|
|
19
|
+
- takahashimm@gmail.com
|
|
20
|
+
executables:
|
|
21
|
+
- equalshares
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- LICENSE.txt
|
|
26
|
+
- README.md
|
|
27
|
+
- exe/equalshares
|
|
28
|
+
- lib/equalshares.rb
|
|
29
|
+
- lib/equalshares/cli.rb
|
|
30
|
+
- lib/equalshares/cli/formatter.rb
|
|
31
|
+
- lib/equalshares/comparison.rb
|
|
32
|
+
- lib/equalshares/completion.rb
|
|
33
|
+
- lib/equalshares/compute.rb
|
|
34
|
+
- lib/equalshares/election.rb
|
|
35
|
+
- lib/equalshares/errors.rb
|
|
36
|
+
- lib/equalshares/fixed_budget.rb
|
|
37
|
+
- lib/equalshares/greedy.rb
|
|
38
|
+
- lib/equalshares/instance.rb
|
|
39
|
+
- lib/equalshares/max_flow.rb
|
|
40
|
+
- lib/equalshares/maximin.rb
|
|
41
|
+
- lib/equalshares/mes_general.rb
|
|
42
|
+
- lib/equalshares/pabulib.rb
|
|
43
|
+
- lib/equalshares/pabulib/csv.rb
|
|
44
|
+
- lib/equalshares/pabulib/parser.rb
|
|
45
|
+
- lib/equalshares/pabulib/project_row_parser.rb
|
|
46
|
+
- lib/equalshares/pabulib/vote_row_parser.rb
|
|
47
|
+
- lib/equalshares/pabulib/writer.rb
|
|
48
|
+
- lib/equalshares/params.rb
|
|
49
|
+
- lib/equalshares/phragmen.rb
|
|
50
|
+
- lib/equalshares/result.rb
|
|
51
|
+
- lib/equalshares/rules/base.rb
|
|
52
|
+
- lib/equalshares/rules/cardinal_mes.rb
|
|
53
|
+
- lib/equalshares/rules/greedy.rb
|
|
54
|
+
- lib/equalshares/rules/maximin.rb
|
|
55
|
+
- lib/equalshares/rules/method_of_equal_shares.rb
|
|
56
|
+
- lib/equalshares/rules/phragmen.rb
|
|
57
|
+
- lib/equalshares/satisfaction.rb
|
|
58
|
+
- lib/equalshares/statistics.rb
|
|
59
|
+
- lib/equalshares/tie_breaker.rb
|
|
60
|
+
- lib/equalshares/tie_breaking.rb
|
|
61
|
+
- lib/equalshares/version.rb
|
|
62
|
+
homepage: https://github.com/takahashim/equalshares
|
|
63
|
+
licenses:
|
|
64
|
+
- MIT
|
|
65
|
+
metadata:
|
|
66
|
+
homepage_uri: https://github.com/takahashim/equalshares
|
|
67
|
+
source_code_uri: https://github.com/takahashim/equalshares
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: 3.2.0
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 4.0.10
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Method of Equal Shares computation for participatory budgeting
|
|
86
|
+
test_files: []
|