sp_npos_elections/phragmen.rs
1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Implementation of the sequential-phragmen election method.
19//!
20//! This method is ensured to achieve PJR, yet, it does not achieve a constant factor approximation
21//! to the Maximin problem.
22
23use crate::{
24 balancing, setup_inputs, BalancingConfig, CandidatePtr, ElectionResult, ExtendedBalance,
25 IdentifierT, PerThing128, VoteWeight, Voter,
26};
27use alloc::vec::Vec;
28use sp_arithmetic::{
29 helpers_128bit::multiply_by_rational_with_rounding,
30 traits::{Bounded, Zero},
31 Rational128, Rounding,
32};
33
34/// The denominator used for loads. Since votes are collected as u64, the smallest ratio that we
35/// might collect is `1/approval_stake` where approval stake is the sum of votes. Hence, some number
36/// bigger than u64::MAX is needed. For maximum accuracy we simply use u128;
37const DEN: ExtendedBalance = ExtendedBalance::max_value();
38
39/// Execute sequential phragmen with potentially some rounds of `balancing`. The return type is list
40/// of winners and a weight distribution vector of all voters who contribute to the winners.
41///
42/// - This function is a best effort to elect `rounds` members. Nonetheless, if less candidates are
43/// available, it will only return what is available. It is the responsibility of the call site to
44/// ensure they have provided enough members.
45/// - If `balance` parameter is `Some(i, t)`, `i` iterations of balancing is with tolerance `t` is
46/// performed.
47/// - Returning winners are sorted based on desirability. Voters are unsorted. Nonetheless,
48/// seq-phragmen is in general an un-ranked election and the desirability should not be
49/// interpreted with any significance.
50/// - The returning winners are zipped with their final backing stake. Yet, to get the exact final
51/// weight distribution from the winner's point of view, one needs to build a support map. See
52/// [`crate::SupportMap`] for more info. Note that this backing stake is computed in
53/// ExtendedBalance and may be slightly different that what will be computed from the support map,
54/// due to accuracy loss.
55/// - The accuracy of the returning edge weight ratios can be configured via the `P` generic
56/// argument.
57/// - The returning weight distribution is _normalized_, meaning that it is guaranteed that the sum
58/// of the ratios in each voter's distribution sums up to exactly `P::one()`.
59///
60/// This can only fail of the normalization fails. This can happen if for any of the resulting
61/// assignments, `assignment.distribution.map(|p| p.deconstruct()).sum()` fails to fit inside
62/// `UpperOf<P>`. A user of this crate may statically assert that this can never happen and safely
63/// `expect` this to return `Ok`.
64///
65/// This can only fail if the normalization fails.
66///
67/// Note that rounding errors can potentially cause the output of this function to fail a t-PJR
68/// check where t is the standard threshold. The underlying algorithm is sound, but the conversions
69/// between numeric types can be lossy.
70pub fn seq_phragmen<AccountId: IdentifierT, P: PerThing128>(
71 to_elect: usize,
72 candidates: Vec<AccountId>,
73 voters: Vec<(AccountId, VoteWeight, impl IntoIterator<Item = AccountId>)>,
74 balancing: Option<BalancingConfig>,
75) -> Result<ElectionResult<AccountId, P>, crate::Error> {
76 let (candidates, voters) = setup_inputs(candidates, voters);
77
78 let (candidates, mut voters) = seq_phragmen_core::<AccountId>(to_elect, candidates, voters)?;
79
80 if let Some(ref config) = balancing {
81 // NOTE: might create zero-edges, but we will strip them again when we convert voter into
82 // assignment.
83 let _iters = balancing::balance::<AccountId>(&mut voters, config);
84 }
85
86 let mut winners = candidates
87 .into_iter()
88 .filter(|c_ptr| c_ptr.borrow().elected)
89 // defensive only: seq-phragmen-core returns only up to rounds.
90 .take(to_elect)
91 .collect::<Vec<_>>();
92
93 // sort winners based on desirability.
94 winners.sort_by_key(|c_ptr| c_ptr.borrow().round);
95
96 let mut assignments =
97 voters.into_iter().filter_map(|v| v.into_assignment()).collect::<Vec<_>>();
98 assignments
99 .iter_mut()
100 .try_for_each(|a| a.try_normalize().map_err(|_| crate::Error::ArithmeticError))?;
101 let winners = winners
102 .into_iter()
103 .map(|w_ptr| (w_ptr.borrow().who.clone(), w_ptr.borrow().backed_stake))
104 .collect();
105
106 Ok(ElectionResult { winners, assignments })
107}
108
109/// Core implementation of seq-phragmen.
110///
111/// This is the internal implementation that works with the types defined in this crate. see
112/// `seq_phragmen` for more information. This function is left public in case a crate needs to use
113/// the implementation in a custom way.
114///
115/// This can only fail if the normalization fails.
116// To create the inputs needed for this function, see [`crate::setup_inputs`].
117pub fn seq_phragmen_core<AccountId: IdentifierT>(
118 to_elect: usize,
119 candidates: Vec<CandidatePtr<AccountId>>,
120 mut voters: Vec<Voter<AccountId>>,
121) -> Result<(Vec<CandidatePtr<AccountId>>, Vec<Voter<AccountId>>), crate::Error> {
122 // we have already checked that we have more candidates than minimum_candidate_count.
123 let to_elect = to_elect.min(candidates.len());
124
125 // main election loop
126 for round in 0..to_elect {
127 // loop 1: initialize score
128 for c_ptr in &candidates {
129 let mut candidate = c_ptr.borrow_mut();
130 if !candidate.elected {
131 // 1 / approval_stake == (DEN / approval_stake) / DEN. If approval_stake is zero,
132 // then the ratio should be as large as possible, essentially `infinity`.
133 if candidate.approval_stake.is_zero() {
134 candidate.score = Bounded::max_value();
135 } else {
136 candidate.score = Rational128::from(DEN / candidate.approval_stake, DEN);
137 }
138 }
139 }
140
141 // loop 2: increment score
142 for voter in &voters {
143 for edge in &voter.edges {
144 let mut candidate = edge.candidate.borrow_mut();
145 if !candidate.elected && !candidate.approval_stake.is_zero() {
146 let temp_n = multiply_by_rational_with_rounding(
147 voter.load.n(),
148 voter.budget,
149 candidate.approval_stake,
150 Rounding::Down,
151 )
152 .unwrap_or(Bounded::max_value());
153 let temp_d = voter.load.d();
154 let temp = Rational128::from(temp_n, temp_d);
155 candidate.score = candidate.score.lazy_saturating_add(temp);
156 }
157 }
158 }
159
160 // loop 3: find the best
161 if let Some(winner_ptr) = candidates
162 .iter()
163 .filter(|c| !c.borrow().elected)
164 .min_by_key(|c| c.borrow().score)
165 {
166 let mut winner = winner_ptr.borrow_mut();
167 // loop 3: update voter and edge load
168 winner.elected = true;
169 winner.round = round;
170 for voter in &mut voters {
171 for edge in &mut voter.edges {
172 if edge.who == winner.who {
173 edge.load = winner.score.lazy_saturating_sub(voter.load);
174 voter.load = winner.score;
175 }
176 }
177 }
178 } else {
179 break
180 }
181 }
182
183 // update backing stake of candidates and voters
184 for voter in &mut voters {
185 for edge in &mut voter.edges {
186 if edge.candidate.borrow().elected {
187 // update internal state.
188 edge.weight = multiply_by_rational_with_rounding(
189 voter.budget,
190 edge.load.n(),
191 voter.load.n(),
192 Rounding::Down,
193 )
194 // If result cannot fit in u128. Not much we can do about it.
195 .unwrap_or(Bounded::max_value());
196 } else {
197 edge.weight = 0
198 }
199 let mut candidate = edge.candidate.borrow_mut();
200 candidate.backed_stake = candidate.backed_stake.saturating_add(edge.weight);
201 }
202
203 // remove all zero edges. These can become phantom edges during normalization.
204 voter.edges.retain(|e| e.weight > 0);
205 // edge of all candidates that eventually have a non-zero weight must be elected.
206 debug_assert!(voter.edges.iter().all(|e| e.candidate.borrow().elected));
207 // inc budget to sum the budget.
208 voter.try_normalize_elected().map_err(|_| crate::Error::ArithmeticError)?;
209 }
210
211 Ok((candidates, voters))
212}