Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix floating point problem of sampling #93

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Fix the floating-point problem of sampling
  • Loading branch information
zemyblue committed Jun 18, 2020
commit 9b65960de085e6aa8006badfb8b011d5aa6e4ed2
30 changes: 13 additions & 17 deletions libs/rand/sampling.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rand

import (
"fmt"
"math"
"math/big"
s "sort"
)
Expand Down Expand Up @@ -74,6 +73,12 @@ func moveWinnerToLast(candidates []Candidate, winner int) {

const uint64Mask = uint64(0x7FFFFFFFFFFFFFFF)

// precisionForSelection is a value to be corrected to increase precision when calculating voting power as an integer.
const precisionForSelection = uint64(1000)

// precisionCorrectionForSelection is a value corrected for accuracy of voting power
const precisionCorrectionForSelection = uint64(1000)

var divider *big.Int

func init() {
Expand All @@ -92,9 +97,6 @@ func randomThreshold(seed *uint64, total uint64) uint64 {
return a.Uint64()
}

// `RandomSamplingWithoutReplacement` elects winners among candidates without replacement
// so it updates rewards of winners. This function continues to elect winners until the both of two
// conditions(minSamplingCount, minPriorityPercent) are met.
func RandomSamplingWithoutReplacement(
seed uint64, candidates []Candidate, minSamplingCount int) (winners []Candidate) {

Expand Down Expand Up @@ -135,22 +137,16 @@ func RandomSamplingWithoutReplacement(
winnerNum, minSamplingCount, winnersPriority, totalPriority, threshold))
}
}
compensationProportions := make([]float64, winnerNum)
for i := winnerNum - 2; i >= 0; i-- { // last winner doesn't get compensation reward
compensationProportions[i] = compensationProportions[i+1] + 1/float64(losersPriorities[i])
correction := totalPriority * precisionForSelection
compensationProportions := make([]uint64, winnerNum)
for i := winnerNum - 2; i >= 0; i-- {
compensationProportions[i] = compensationProportions[i+1] + correction/losersPriorities[i]
}
winners = candidates[len(candidates)-winnerNum:]
winPoints := make([]float64, len(winners))
totalWinPoint := float64(0)
winPoints := make([]uint64, len(winners))
for i, winner := range winners {
winPoints[i] = 1 + float64(winner.Priority())*compensationProportions[i]
totalWinPoint += winPoints[i]
}
for i, winner := range winners {
if winPoints[i] > math.MaxInt64 || winPoints[i] < 0 {
panic(fmt.Sprintf("winPoint is invalid: %f", winPoints[i]))
}
winner.SetWinPoint(int64(float64(totalPriority) * winPoints[i] / totalWinPoint))
winPoints[i] = correction + winner.Priority()*compensationProportions[i]
winner.SetWinPoint(int64(winPoints[i] / (correction / precisionCorrectionForSelection)))
}
return winners
}
Expand Down
21 changes: 16 additions & 5 deletions libs/rand/sampling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestRandomSamplingWithoutReplacement1Candidate(t *testing.T) {
winners := RandomSamplingWithoutReplacement(0, candidates, 1)
assert.True(t, len(winners) == 1)
assert.True(t, candidates[0] == winners[0])
assert.True(t, winners[0].(*Element).winPoint == 1000)
assert.True(t, uint64(winners[0].(*Element).winPoint) == precisionForSelection)
resetWinPoint(candidates)

winners2 := RandomSamplingWithoutReplacement(0, candidates, 0)
Expand Down Expand Up @@ -176,11 +176,14 @@ func TestRandomSamplingWithoutReplacementIncludingZeroStakingPower(t *testing.T)
assert.True(t, len(winners2) == 90)
}

func accumulateAndResetReward(candidate []Candidate, acc []uint64) {
func accumulateAndResetReward(candidate []Candidate, acc []uint64) uint64 {
totalWinPoint := uint64(0)
for i, c := range candidate {
acc[i] += uint64(c.(*Element).winPoint)
totalWinPoint += uint64(c.(*Element).winPoint)
c.(*Element).winPoint = 0
}
return totalWinPoint
}

func TestDivider(t *testing.T) {
Expand Down Expand Up @@ -277,14 +280,22 @@ func TestRandomSamplingWithoutReplacementEquity(t *testing.T) {

// good condition
candidates := newCandidates(100, func(i int) uint64 { return 1000000 + rand.Uint64()&0xFFFFF })
totalStaking := uint64(0)
for _, c := range candidates {
totalStaking += c.Priority()
}

accumulatedRewards := make([]uint64, 100)
totalAccumulateRewards := uint64(0)
for i := 0; i < loopCount; i++ {
RandomSamplingWithoutReplacement(uint64(i), candidates, 99)
accumulateAndResetReward(candidates, accumulatedRewards)
totalAccumulateRewards += accumulateAndResetReward(candidates, accumulatedRewards)
}
for i := 0; i < 99; i++ {
rewardPerStakingDiff :=
math.Abs(float64(accumulatedRewards[i])/float64(candidates[i].Priority())/float64(loopCount) - 1)
rewardRate := float64(accumulatedRewards[i]) / float64(totalAccumulateRewards)
stakingRate := float64(candidates[i].Priority()) / float64(totalStaking)
rate := rewardRate / stakingRate
rewardPerStakingDiff := math.Abs(1 - rate)
assert.True(t, rewardPerStakingDiff < 0.01)
}

Expand Down