Generic Normalize impl for arithmetic and npos-elections (#6374)

* add normalize

* better api for normalize

* Some grumbles

* Update primitives/arithmetic/src/lib.rs

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* More great review grumbles

* Way better doc for everything.

* Some improvement

* Update primitives/arithmetic/src/lib.rs

Co-authored-by: Bernhard Schuster <bernhard@ahoi.io>

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Bernhard Schuster <bernhard@ahoi.io>
This commit is contained in:
Kian Paimani
2020-06-24 15:32:50 +02:00
committed by GitHub
parent b14b472edf
commit e016a49322
15 changed files with 809 additions and 246 deletions
@@ -17,37 +17,72 @@
//! Helper methods for npos-elections.
use crate::{Assignment, ExtendedBalance, VoteWeight, IdentifierT, StakedAssignment, WithApprovalOf};
use sp_arithmetic::PerThing;
use crate::{Assignment, ExtendedBalance, VoteWeight, IdentifierT, StakedAssignment, WithApprovalOf, Error};
use sp_arithmetic::{PerThing, InnerOf};
use sp_std::prelude::*;
/// Converts a vector of ratio assignments into ones with absolute budget value.
pub fn assignment_ratio_to_staked<A: IdentifierT, T: PerThing, FS>(
ratio: Vec<Assignment<A, T>>,
///
/// Note that this will NOT attempt at normalizing the result.
pub fn assignment_ratio_to_staked<A: IdentifierT, P: PerThing, FS>(
ratio: Vec<Assignment<A, P>>,
stake_of: FS,
) -> Vec<StakedAssignment<A>>
where
for<'r> FS: Fn(&'r A) -> VoteWeight,
T: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
ExtendedBalance: From<<T as PerThing>::Inner>,
P: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
ExtendedBalance: From<InnerOf<P>>,
{
ratio
.into_iter()
.map(|a| {
let stake = stake_of(&a.who);
a.into_staked(stake.into(), true)
a.into_staked(stake.into())
})
.collect()
}
/// Converts a vector of staked assignments into ones with ratio values.
pub fn assignment_staked_to_ratio<A: IdentifierT, T: PerThing>(
staked: Vec<StakedAssignment<A>>,
) -> Vec<Assignment<A, T>>
/// Same as [`assignment_ratio_to_staked`] and try and do normalization.
pub fn assignment_ratio_to_staked_normalized<A: IdentifierT, P: PerThing, FS>(
ratio: Vec<Assignment<A, P>>,
stake_of: FS,
) -> Result<Vec<StakedAssignment<A>>, Error>
where
ExtendedBalance: From<<T as PerThing>::Inner>,
for<'r> FS: Fn(&'r A) -> VoteWeight,
P: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
ExtendedBalance: From<InnerOf<P>>,
{
staked.into_iter().map(|a| a.into_assignment(true)).collect()
let mut staked = assignment_ratio_to_staked(ratio, &stake_of);
staked.iter_mut().map(|a|
a.try_normalize(stake_of(&a.who).into()).map_err(|err| Error::ArithmeticError(err))
).collect::<Result<_, _>>()?;
Ok(staked)
}
/// Converts a vector of staked assignments into ones with ratio values.
///
/// Note that this will NOT attempt at normalizing the result.
pub fn assignment_staked_to_ratio<A: IdentifierT, P: PerThing>(
staked: Vec<StakedAssignment<A>>,
) -> Vec<Assignment<A, P>>
where
ExtendedBalance: From<InnerOf<P>>,
{
staked.into_iter().map(|a| a.into_assignment()).collect()
}
/// Same as [`assignment_staked_to_ratio`] and try and do normalization.
pub fn assignment_staked_to_ratio_normalized<A: IdentifierT, P: PerThing>(
staked: Vec<StakedAssignment<A>>,
) -> Result<Vec<Assignment<A, P>>, Error>
where
ExtendedBalance: From<InnerOf<P>>,
{
let mut ratio = staked.into_iter().map(|a| a.into_assignment()).collect::<Vec<_>>();
ratio.iter_mut().map(|a|
a.try_normalize().map_err(|err| Error::ArithmeticError(err))
).collect::<Result<_, _>>()?;
Ok(ratio)
}
/// consumes a vector of winners with backing stake to just winners.
+60 -54
View File
@@ -30,7 +30,7 @@
use sp_std::{prelude::*, collections::btree_map::BTreeMap, fmt::Debug, cmp::Ordering, convert::TryFrom};
use sp_arithmetic::{
PerThing, Rational128, ThresholdOrd,
PerThing, Rational128, ThresholdOrd, InnerOf, Normalizable,
helpers_128bit::multiply_by_rational,
traits::{Zero, Saturating, Bounded, SaturatedConversion},
};
@@ -84,6 +84,8 @@ pub enum Error {
CompactTargetOverflow,
/// One of the index functions returned none.
CompactInvalidIndex,
/// An error occurred in some arithmetic operation.
ArithmeticError(&'static str),
}
/// A type which is used in the API of this crate as a numeric weight of a vote, most often the
@@ -155,16 +157,16 @@ pub struct ElectionResult<AccountId, T: PerThing> {
/// A voter's stake assignment among a set of targets, represented as ratios.
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "std", derive(PartialEq, Eq, Encode, Decode))]
pub struct Assignment<AccountId, T: PerThing> {
pub struct Assignment<AccountId, P: PerThing> {
/// Voter's identifier.
pub who: AccountId,
/// The distribution of the voter's stake.
pub distribution: Vec<(AccountId, T)>,
pub distribution: Vec<(AccountId, P)>,
}
impl<AccountId, T: PerThing> Assignment<AccountId, T>
impl<AccountId: IdentifierT, P: PerThing> Assignment<AccountId, P>
where
ExtendedBalance: From<<T as PerThing>::Inner>,
ExtendedBalance: From<InnerOf<P>>,
{
/// Convert from a ratio assignment into one with absolute values aka. [`StakedAssignment`].
///
@@ -173,50 +175,49 @@ where
/// distribution's sum is exactly equal to the total budget, by adding or subtracting the
/// remainder from the last distribution.
///
/// If an edge ratio is [`Bounded::max_value()`], it is dropped. This edge can never mean
/// If an edge ratio is [`Bounded::min_value()`], it is dropped. This edge can never mean
/// anything useful.
pub fn into_staked(self, stake: ExtendedBalance, fill: bool) -> StakedAssignment<AccountId>
pub fn into_staked(self, stake: ExtendedBalance) -> StakedAssignment<AccountId>
where
T: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
P: sp_std::ops::Mul<ExtendedBalance, Output = ExtendedBalance>,
{
let mut sum: ExtendedBalance = Bounded::min_value();
let mut distribution = self
.distribution
let distribution = self.distribution
.into_iter()
.filter_map(|(target, p)| {
// if this ratio is zero, then skip it.
if p == Bounded::min_value() {
if p.is_zero() {
None
} else {
// NOTE: this mul impl will always round to the nearest number, so we might both
// overflow and underflow.
let distribution_stake = p * stake;
// defensive only. We assume that balance cannot exceed extended balance.
sum = sum.saturating_add(distribution_stake);
Some((target, distribution_stake))
}
})
.collect::<Vec<(AccountId, ExtendedBalance)>>();
if fill {
// NOTE: we can do this better.
// https://revs.runtime-revolution.com/getting-100-with-rounded-percentages-273ffa70252b
if let Some(leftover) = stake.checked_sub(sum) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_add(leftover);
}
} else if let Some(excess) = sum.checked_sub(stake) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_sub(excess);
}
}
}
StakedAssignment {
who: self.who,
distribution,
}
}
/// Try and normalize this assignment.
///
/// If `Ok(())` is returned, then the assignment MUST have been successfully normalized to 100%.
pub fn try_normalize(&mut self) -> Result<(), &'static str> {
self.distribution
.iter()
.map(|(_, p)| *p)
.collect::<Vec<_>>()
.normalize(P::one())
.map(|normalized_ratios|
self.distribution
.iter_mut()
.zip(normalized_ratios)
.for_each(|((_, old), corrected)| { *old = corrected; })
)
}
}
/// A voter's stake assignment among a set of targets, represented as absolute values in the scale
@@ -243,42 +244,23 @@ impl<AccountId> StakedAssignment<AccountId> {
///
/// If an edge stake is so small that it cannot be represented in `T`, it is ignored. This edge
/// can never be re-created and does not mean anything useful anymore.
pub fn into_assignment<T: PerThing>(self, fill: bool) -> Assignment<AccountId, T>
pub fn into_assignment<P: PerThing>(self) -> Assignment<AccountId, P>
where
ExtendedBalance: From<<T as PerThing>::Inner>,
ExtendedBalance: From<InnerOf<P>>,
AccountId: IdentifierT,
{
let accuracy: u128 = T::ACCURACY.saturated_into();
let mut sum: u128 = Zero::zero();
let stake = self.distribution.iter().map(|x| x.1).sum();
let mut distribution = self
.distribution
let stake = self.total();
let distribution = self.distribution
.into_iter()
.filter_map(|(target, w)| {
let per_thing = T::from_rational_approximation(w, stake);
let per_thing = P::from_rational_approximation(w, stake);
if per_thing == Bounded::min_value() {
None
} else {
sum += per_thing.clone().deconstruct().saturated_into();
Some((target, per_thing))
}
})
.collect::<Vec<(AccountId, T)>>();
if fill {
if let Some(leftover) = accuracy.checked_sub(sum) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_add(
T::from_parts(leftover.saturated_into())
);
}
} else if let Some(excess) = sum.checked_sub(accuracy) {
if let Some(last) = distribution.last_mut() {
last.1 = last.1.saturating_sub(
T::from_parts(excess.saturated_into())
);
}
}
}
.collect::<Vec<(AccountId, P)>>();
Assignment {
who: self.who,
@@ -286,6 +268,30 @@ impl<AccountId> StakedAssignment<AccountId> {
}
}
/// Try and normalize this assignment.
///
/// If `Ok(())` is returned, then the assignment MUST have been successfully normalized to
/// `stake`.
///
/// NOTE: current implementation of `.normalize` is almost safe to `expect()` upon. The only
/// error case is when the input cannot fit in `T`, or the sum of input cannot fit in `T`.
/// Sadly, both of these are dependent upon the implementation of `VoteLimit`, i.e. the limit
/// of edges per voter which is enforced from upstream. Hence, at this crate, we prefer
/// returning a result and a use the name prefix `try_`.
pub fn try_normalize(&mut self, stake: ExtendedBalance) -> Result<(), &'static str> {
self.distribution
.iter()
.map(|(_, ref weight)| *weight)
.collect::<Vec<_>>()
.normalize(stake)
.map(|normalized_weights|
self.distribution
.iter_mut()
.zip(normalized_weights.into_iter())
.for_each(|((_, weight), corrected)| { *weight = corrected; })
)
}
/// Get the total stake of this assignment (aka voter budget).
pub fn total(&self) -> ExtendedBalance {
self.distribution.iter().fold(Zero::zero(), |a, b| a.saturating_add(b.1))
+246 -154
View File
@@ -588,186 +588,278 @@ fn self_votes_should_be_kept() {
);
}
#[test]
fn assignment_convert_works() {
let staked = StakedAssignment {
who: 1 as AccountId,
distribution: vec![
(20, 100 as ExtendedBalance),
(30, 25),
],
};
mod assignment_convert_normalize {
use super::*;
#[test]
fn assignment_convert_works() {
let staked = StakedAssignment {
who: 1 as AccountId,
distribution: vec![
(20, 100 as ExtendedBalance),
(30, 25),
],
};
let assignment = staked.clone().into_assignment(true);
assert_eq!(
assignment,
Assignment {
let assignment = staked.clone().into_assignment();
assert_eq!(
assignment,
Assignment {
who: 1,
distribution: vec![
(20, Perbill::from_percent(80)),
(30, Perbill::from_percent(20)),
]
}
);
assert_eq!(
assignment.into_staked(125),
staked,
);
}
#[test]
fn assignment_convert_will_not_normalize() {
assert_eq!(
Assignment {
who: 1,
distribution: vec![
(2, Perbill::from_percent(33)),
(3, Perbill::from_percent(66)),
]
}.into_staked(100),
StakedAssignment {
who: 1,
distribution: vec![
(2, 33),
(3, 66),
// sum is not 100!
],
},
);
assert_eq!(
StakedAssignment {
who: 1,
distribution: vec![
(2, 333_333_333_333_333),
(3, 333_333_333_333_333),
(4, 666_666_666_666_333),
],
}.into_assignment(),
Assignment {
who: 1,
distribution: vec![
(2, Perbill::from_parts(250000000)),
(3, Perbill::from_parts(250000000)),
(4, Perbill::from_parts(499999999)),
// sum is not 100%!
]
},
)
}
#[test]
fn assignment_can_normalize() {
let mut a = Assignment {
who: 1,
distribution: vec![
(20, Perbill::from_percent(80)),
(30, Perbill::from_percent(20)),
(2, Perbill::from_parts(330000000)),
(3, Perbill::from_parts(660000000)),
// sum is not 100%!
]
};
a.try_normalize().unwrap();
assert_eq!(
a,
Assignment {
who: 1,
distribution: vec![
(2, Perbill::from_parts(340000000)),
(3, Perbill::from_parts(660000000)),
]
},
);
}
#[test]
fn staked_assignment_can_normalize() {
let mut a = StakedAssignment {
who: 1,
distribution: vec![
(2, 33),
(3, 66),
]
};
a.try_normalize(100).unwrap();
assert_eq!(
a,
StakedAssignment {
who: 1,
distribution: vec![
(2, 34),
(3, 66),
]
},
);
}
}
mod score {
use super::*;
#[test]
fn score_comparison_is_lexicographical_no_epsilon() {
let epsilon = Perbill::zero();
// only better in the fist parameter, worse in the other two ✅
assert_eq!(
is_score_better([12, 10, 35], [10, 20, 30], epsilon),
true,
);
// worse in the first, better in the other two ❌
assert_eq!(
is_score_better([9, 30, 10], [10, 20, 30], epsilon),
false,
);
// equal in the first, the second one dictates.
assert_eq!(
is_score_better([10, 25, 40], [10, 20, 30], epsilon),
true,
);
// equal in the first two, the last one dictates.
assert_eq!(
is_score_better([10, 20, 40], [10, 20, 30], epsilon),
false,
);
}
#[test]
fn score_comparison_with_epsilon() {
let epsilon = Perbill::from_percent(1);
{
// no more than 1 percent (10) better in the first param.
assert_eq!(
is_score_better([1009, 5000, 100000], [1000, 5000, 100000], epsilon),
false,
);
// now equal, still not better.
assert_eq!(
is_score_better([1010, 5000, 100000], [1000, 5000, 100000], epsilon),
false,
);
// now it is.
assert_eq!(
is_score_better([1011, 5000, 100000], [1000, 5000, 100000], epsilon),
true,
);
}
);
assert_eq!(
assignment.into_staked(125, true),
staked,
);
}
{
// First score score is epsilon better, but first score is no longer `ge`. Then this is
// still not a good solution.
assert_eq!(
is_score_better([999, 6000, 100000], [1000, 5000, 100000], epsilon),
false,
);
}
#[test]
fn score_comparison_is_lexicographical_no_epsilon() {
let epsilon = Perbill::zero();
// only better in the fist parameter, worse in the other two ✅
assert_eq!(
is_score_better([12, 10, 35], [10, 20, 30], epsilon),
true,
);
{
// first score is equal or better, but not epsilon. Then second one is the determinant.
assert_eq!(
is_score_better([1005, 5000, 100000], [1000, 5000, 100000], epsilon),
false,
);
// worse in the first, better in the other two ❌
assert_eq!(
is_score_better([9, 30, 10], [10, 20, 30], epsilon),
false,
);
assert_eq!(
is_score_better([1005, 5050, 100000], [1000, 5000, 100000], epsilon),
false,
);
// equal in the first, the second one dictates.
assert_eq!(
is_score_better([10, 25, 40], [10, 20, 30], epsilon),
true,
);
assert_eq!(
is_score_better([1005, 5051, 100000], [1000, 5000, 100000], epsilon),
true,
);
}
// equal in the first two, the last one dictates.
assert_eq!(
is_score_better([10, 20, 40], [10, 20, 30], epsilon),
false,
);
}
{
// first score and second are equal or less than epsilon more, third is determinant.
assert_eq!(
is_score_better([1005, 5025, 100000], [1000, 5000, 100000], epsilon),
false,
);
#[test]
fn score_comparison_with_epsilon() {
let epsilon = Perbill::from_percent(1);
assert_eq!(
is_score_better([1005, 5025, 99_000], [1000, 5000, 100000], epsilon),
false,
);
assert_eq!(
is_score_better([1005, 5025, 98_999], [1000, 5000, 100000], epsilon),
true,
);
}
}
#[test]
fn score_comparison_large_value() {
// some random value taken from eras in kusama.
let initial = [12488167277027543u128, 5559266368032409496, 118749283262079244270992278287436446];
// this claim is 0.04090% better in the third component. It should be accepted as better if
// epsilon is smaller than 5/10_0000
let claim = [12488167277027543u128, 5559266368032409496, 118700736389524721358337889258988054];
{
// no more than 1 percent (10) better in the first param.
assert_eq!(
is_score_better([1009, 5000, 100000], [1000, 5000, 100000], epsilon),
false,
);
// now equal, still not better.
assert_eq!(
is_score_better([1010, 5000, 100000], [1000, 5000, 100000], epsilon),
false,
);
// now it is.
assert_eq!(
is_score_better([1011, 5000, 100000], [1000, 5000, 100000], epsilon),
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(1u32, 10_000),
),
true,
);
}
{
// First score score is epsilon better, but first score is no longer `ge`. Then this is
// still not a good solution.
assert_eq!(
is_score_better([999, 6000, 100000], [1000, 5000, 100000], epsilon),
false,
);
}
{
// first score is equal or better, but not epsilon. Then second one is the determinant.
assert_eq!(
is_score_better([1005, 5000, 100000], [1000, 5000, 100000], epsilon),
false,
);
assert_eq!(
is_score_better([1005, 5050, 100000], [1000, 5000, 100000], epsilon),
false,
);
assert_eq!(
is_score_better([1005, 5051, 100000], [1000, 5000, 100000], epsilon),
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(2u32, 10_000),
),
true,
);
}
{
// first score and second are equal or less than epsilon more, third is determinant.
assert_eq!(
is_score_better([1005, 5025, 100000], [1000, 5000, 100000], epsilon),
false,
);
assert_eq!(
is_score_better([1005, 5025, 99_000], [1000, 5000, 100000], epsilon),
false,
);
assert_eq!(
is_score_better([1005, 5025, 98_999], [1000, 5000, 100000], epsilon),
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(3u32, 10_000),
),
true,
);
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(4u32, 10_000),
),
true,
);
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(5u32, 10_000),
),
false,
);
}
}
#[test]
fn score_comparison_large_value() {
// some random value taken from eras in kusama.
let initial = [12488167277027543u128, 5559266368032409496, 118749283262079244270992278287436446];
// this claim is 0.04090% better in the third component. It should be accepted as better if
// epsilon is smaller than 5/10_0000
let claim = [12488167277027543u128, 5559266368032409496, 118700736389524721358337889258988054];
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(1u32, 10_000),
),
true,
);
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(2u32, 10_000),
),
true,
);
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(3u32, 10_000),
),
true,
);
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(4u32, 10_000),
),
true,
);
assert_eq!(
is_score_better(
claim.clone(),
initial.clone(),
Perbill::from_rational_approximation(5u32, 10_000),
),
false,
);
}
mod compact {
use codec::{Decode, Encode};
use super::AccountId;