2018-12-10 13:23:18 +03:00
|
|
|
package autopilot
|
|
|
|
|
|
|
|
import (
|
2018-12-10 13:23:19 +03:00
|
|
|
"errors"
|
2018-12-10 13:23:18 +03:00
|
|
|
"fmt"
|
|
|
|
"math/rand"
|
|
|
|
)
|
|
|
|
|
2018-12-10 13:23:19 +03:00
|
|
|
// ErrNoPositive is returned from weightedChoice when there are no positive
|
|
|
|
// weights left to choose from.
|
|
|
|
var ErrNoPositive = errors.New("no positive weights left")
|
|
|
|
|
2018-12-10 13:23:19 +03:00
|
|
|
// weightedChoice draws a random index from the slice of weights, with a
|
|
|
|
// probability propotional to the weight at the given index.
|
|
|
|
func weightedChoice(w []float64) (int, error) {
|
|
|
|
// Calculate the sum of weights.
|
2018-12-10 13:23:18 +03:00
|
|
|
var sum float64
|
2018-12-10 13:23:19 +03:00
|
|
|
for _, v := range w {
|
|
|
|
sum += v
|
2018-12-10 13:23:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if sum <= 0 {
|
2018-12-10 13:23:19 +03:00
|
|
|
return 0, ErrNoPositive
|
2018-12-10 13:23:18 +03:00
|
|
|
}
|
|
|
|
|
2018-12-10 13:23:19 +03:00
|
|
|
// Pick a random number in the range [0.0, 1.0) and multiply it with
|
|
|
|
// the sum of weights. Then we'll iterate the weights until the number
|
|
|
|
// goes below 0. This means that each index is picked with a probablity
|
|
|
|
// equal to their normalized score.
|
2018-12-10 13:23:18 +03:00
|
|
|
//
|
|
|
|
// Example:
|
|
|
|
// Items with scores [1, 5, 2, 2]
|
|
|
|
// Normalized scores [0.1, 0.5, 0.2, 0.2]
|
|
|
|
// Imagine they each occupy a "range" equal to their normalized score
|
|
|
|
// in [0, 1.0]:
|
|
|
|
// [|-0.1-||-----0.5-----||--0.2--||--0.2--|]
|
|
|
|
// The following loop is now equivalent to "hitting" the intervals.
|
2018-12-10 13:23:19 +03:00
|
|
|
r := rand.Float64() * sum
|
|
|
|
for i := range w {
|
|
|
|
r -= w[i]
|
2018-12-10 13:23:18 +03:00
|
|
|
if r <= 0 {
|
2018-12-10 13:23:19 +03:00
|
|
|
return i, nil
|
2018-12-10 13:23:18 +03:00
|
|
|
}
|
|
|
|
}
|
2018-12-10 13:23:19 +03:00
|
|
|
|
|
|
|
return 0, fmt.Errorf("unable to make choice")
|
2018-12-10 13:23:18 +03:00
|
|
|
}
|
|
|
|
|
2018-12-19 17:22:33 +03:00
|
|
|
// chooseN picks at random min[n, len(s)] nodes if from the NodeScore map, with
|
|
|
|
// a probability weighted by their score.
|
|
|
|
func chooseN(n uint32, s map[NodeID]*NodeScore) (
|
|
|
|
map[NodeID]*NodeScore, error) {
|
2018-12-10 13:23:18 +03:00
|
|
|
|
2018-12-10 13:23:19 +03:00
|
|
|
// Keep track of the number of nodes not yet chosen, in addition to
|
|
|
|
// their scores and NodeIDs.
|
|
|
|
rem := len(s)
|
|
|
|
scores := make([]float64, len(s))
|
|
|
|
nodeIDs := make([]NodeID, len(s))
|
|
|
|
i := 0
|
2018-12-10 13:23:18 +03:00
|
|
|
for k, v := range s {
|
2018-12-10 13:23:19 +03:00
|
|
|
scores[i] = v.Score
|
|
|
|
nodeIDs[i] = k
|
|
|
|
i++
|
2018-12-10 13:23:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Pick a weighted choice from the remaining nodes as long as there are
|
|
|
|
// nodes left, and we haven't already picked n.
|
2018-12-19 17:22:33 +03:00
|
|
|
chosen := make(map[NodeID]*NodeScore)
|
2018-12-10 13:23:19 +03:00
|
|
|
for len(chosen) < int(n) && rem > 0 {
|
|
|
|
choice, err := weightedChoice(scores)
|
2018-12-10 13:23:19 +03:00
|
|
|
if err == ErrNoPositive {
|
|
|
|
return chosen, nil
|
|
|
|
} else if err != nil {
|
2018-12-10 13:23:18 +03:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-12-10 13:23:19 +03:00
|
|
|
nID := nodeIDs[choice]
|
|
|
|
|
|
|
|
chosen[nID] = s[nID]
|
|
|
|
|
|
|
|
// We set the score of the chosen node to 0, so it won't be
|
|
|
|
// picked the next iteration.
|
|
|
|
scores[choice] = 0
|
2018-12-10 13:23:18 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return chosen, nil
|
|
|
|
}
|