From 541aa5d6cf1ebb82514829cc7ce2feddff2a2fe6 Mon Sep 17 00:00:00 2001 From: Brian Amadio Date: Wed, 24 Feb 2021 18:16:32 -0800 Subject: [PATCH] Adds HTTPSource and a lot of tests (#2) --- bandit.go | 58 ++-- bandit_test.go | 2 +- dists.go | 31 ++- epsilon_greedy.go | 42 ++- go.mod | 9 +- go.sum | 399 +--------------------------- http_reward_source.go | 201 ++++++++++++++ mab_test/epsilon_greedy_test.go | 250 +++++++++++++++++ mab_test/http_reward_source_test.go | 287 ++++++++++++++++++++ mab_test/mab_test.go | 27 +- mab_test/proportional_test.go | 109 ++++++++ mab_test/sha1sampler_test.go | 115 ++++++++ mab_test/thompson_mc_test.go | 26 ++ mab_test/thompson_test.go | 251 +++++++++++++++++ numint/quadrature.go | 3 + proportional.go | 30 ++- reward_source.go | 13 - reward_stub.go | 38 +++ sha1sampler.go | 8 +- sha1sampler_internal_test.go | 32 ++- thompson.go | 4 + thompson_mc.go | 6 + thompson_mc_test.go | 24 -- thompson_test.go | 26 -- 24 files changed, 1492 insertions(+), 499 deletions(-) create mode 100644 http_reward_source.go create mode 100644 mab_test/epsilon_greedy_test.go create mode 100644 mab_test/http_reward_source_test.go create mode 100644 mab_test/proportional_test.go create mode 100644 mab_test/sha1sampler_test.go create mode 100644 mab_test/thompson_mc_test.go create mode 100644 mab_test/thompson_test.go delete mode 100644 reward_source.go create mode 100644 reward_stub.go delete mode 100644 thompson_mc_test.go delete mode 100644 thompson_test.go diff --git a/bandit.go b/bandit.go index 36e2690..28cfa5f 100644 --- a/bandit.go +++ b/bandit.go @@ -4,7 +4,7 @@ import ( "context" ) -// Bandit gets reward values from a RewardSource, computes selection probabilities using a Strategy, and selects +// A Bandit gets reward values from a RewardSource, computes selection probabilities using a Strategy, and selects // an arm using a Sampler. type Bandit struct { RewardSource @@ -12,14 +12,17 @@ type Bandit struct { Sampler } -type Result struct { - Rewards []Dist - Probs []float64 - Arm int -} - -// SelectArm gets the current reward estimates, computes the arm selection probability, and selects and arm index. -func (b *Bandit) SelectArm(ctx context.Context, unit string) (Result, error) { +// SelectArm gets the current reward estimates, computes the arm selection probabilities, and selects and arm index. +// Returns a partial result and an error message if an error is encountered at any point. +// For example, if the reward estimates were retrieved, but an error was encountered during the probability computation, +// the result will contain the reward estimates, but no probabilities or arm index. +// There is an unfortunate name collision between a multi-armed bandit context and Go's context.Context type. +// The context.Context argument should only be used for passing request-scoped data to an external reward service, such +// as timeouts and cancellation propagation. +// The banditContext argument is used to pass bandit context features to the reward source for contextual bandits. +// The unit argument is a string that will be hashed to select an arm with the pseudo-random sampler. +// SelectArm is deterministic for a fixed unit and set of reward estimates from the RewardSource. +func (b *Bandit) SelectArm(ctx context.Context, unit string, banditContext interface{}) (Result, error) { res := Result{ Rewards: make([]Dist, 0), @@ -27,7 +30,7 @@ func (b *Bandit) SelectArm(ctx context.Context, unit string) (Result, error) { Arm: -1, } - rewards, err := b.GetRewards(ctx) + rewards, err := b.GetRewards(ctx, banditContext) if err != nil { return res, err } @@ -51,14 +54,19 @@ func (b *Bandit) SelectArm(ctx context.Context, unit string) (Result, error) { return res, nil } -// RewardSource provides the current reward estimates, as a Dist for each arm. -// Features can be passed to the RewardSource using the Context argument, which is useful for contextual bandits. -// The RewardSource should provide the reward estimates conditioned on those context features. -type RewardSource interface { - GetRewards(context.Context) ([]Dist, error) +// Result is the return type for a call to Bandit.SelectArm. +// It will contain the reward estimates provided by the RewardSource, the computed arm selection probabilities, +// and the index of the selected arm. +type Result struct { + Rewards []Dist + Probs []float64 + Arm int } -// Dist represents a one-dimensional probability distribution. +// A Dist represents a one-dimensional probability distribution. +// Reward estimates are represented as a Dist for each arm. +// Strategies compute arm-selection probabilities using the Dist interface. +// This allows for combining different distributions with different strategies. type Dist interface { // CDF returns the cumulative distribution function evaluated at x. CDF(x float64) float64 @@ -76,14 +84,24 @@ type Dist interface { Support() (float64, float64) } -// Strategy computes arm selection probabilities from a slice of Distributions. -// The output probabilities slice should be the same length as the input Dist slice. +// A RewardSource provides the current reward estimates, in the form of a Dist for each arm. +// There is an unfortunate name collision between a multi-armed bandit context and Go's Context type. +// The first argument is a context.Context and should only be used for passing request-scoped data to an external reward service. +// If the RewardSource does not require an external request, this first argument should always be context.Background() +// The second argument is used to pass context values to the reward source for contextual bandits. +// A RewardSource implementation should provide the reward estimates conditioned on the value of banditContext. +// For non-contextual bandits, banditContext can be nil. +type RewardSource interface { + GetRewards(ctx context.Context, banditContext interface{}) ([]Dist, error) +} + +// A Strategy computes arm selection probabilities from a slice of Distributions. type Strategy interface { ComputeProbs([]Dist) ([]float64, error) } -// Sampler returns a pseudo-random arm index given a set of probabilities and a unit. -// Samplers should always return the same arm index for the same set of probabilities and unit. +// A Sampler returns a pseudo-random arm index given a set of probabilities and a string to hash. +// Samplers should always return the same arm index for the same set of probabilities and unit value. type Sampler interface { Sample(probs []float64, unit string) (int, error) } diff --git a/bandit_test.go b/bandit_test.go index 884774c..51148ea 100644 --- a/bandit_test.go +++ b/bandit_test.go @@ -24,7 +24,7 @@ func ExampleBandit_SelectArm() { Sampler: NewSha1Sampler(), } - result, err := b.SelectArm(context.Background(), "12345") + result, err := b.SelectArm(context.Background(), "12345", nil) if err != nil { panic(err) } diff --git a/dists.go b/dists.go index 9586f27..85af1b3 100644 --- a/dists.go +++ b/dists.go @@ -7,6 +7,8 @@ import ( "gonum.org/v1/gonum/stat/distuv" ) +// Normal is a normal distribution for use with any bandit strategy. +// For the purposes of Thompson sampling, it is truncated at mean +/- 4*sigma func Normal(mu, sigma float64) NormalDist { return NormalDist{distuv.Normal{Mu: mu, Sigma: sigma}} } @@ -24,6 +26,7 @@ func (n NormalDist) String() string { return fmt.Sprintf("Normal(%f,%f)", n.Mu, n.Sigma) } +// Beta is a beta distribution for use with any bandit strategy. func Beta(alpha, beta float64) BetaDist { return BetaDist{distuv.Beta{Alpha: alpha, Beta: beta}} } @@ -40,40 +43,50 @@ func (b BetaDist) String() string { return fmt.Sprintf("Beta(%f,%f)", b.Beta.Alpha, b.Beta.Beta) } -func Point(x float64) PointDist { - return PointDist{x} +// Point is used for reward models that just provide point estimates. Don't use with Thompson sampling. +func Point(mu float64) PointDist { + return PointDist{mu} } type PointDist struct { - X float64 + Mu float64 } func (p PointDist) Mean() float64 { - return p.X + return p.Mu } func (p PointDist) CDF(x float64) float64 { - if x >= p.X { + if x >= p.Mu { return 1 } return 0 } func (p PointDist) Prob(x float64) float64 { - if x == p.X { + if x == p.Mu { return math.NaN() } return 0 } func (p PointDist) Rand() float64 { - return p.X + return p.Mu } func (p PointDist) Support() (float64, float64) { - return p.X, p.X + return p.Mu, p.Mu } func (p PointDist) String() string { - return fmt.Sprintf("Point(%f)", p.X) + if math.IsInf(p.Mu, -1) { + return "Null()" + } + return fmt.Sprintf("Point(%f)", p.Mu) +} + +// Null returns a PointDist with mean equal to negative infinity. This is a special value that indicates +// to a Strategy that this arm should get selection probability zero. +func Null() PointDist { + return PointDist{math.Inf(-1)} } diff --git a/epsilon_greedy.go b/epsilon_greedy.go index 7addc5c..cbc9400 100644 --- a/epsilon_greedy.go +++ b/epsilon_greedy.go @@ -5,17 +5,34 @@ import ( "math" ) +func NewEpsilonGreedy(e float64) *EpsilonGreedy { + return &EpsilonGreedy{ + Epsilon: e, + } +} + +// EpsilonGreedy implements the epsilon-greedy bandit strategy. +// The Epsilon parameter must be greater than zero. +// If any arm has a Null distribution, it will have zero selection probability, and the other +// arms' probabilities will be computed as if the Null arms are not present. +// Ties are accounted for, so if multiple arms have the maximum mean reward estimate, they will have equal probabilities. type EpsilonGreedy struct { Epsilon float64 meanRewards []float64 } +// ComputeProbs computes the arm selection probabilities from the set of reward estimates, accounting for Nulls and ties. +// Returns an error if epsilon is less than zero. func (e *EpsilonGreedy) ComputeProbs(rewards []Dist) ([]float64, error) { if err := e.validateEpsilon(); err != nil { return nil, err } + if len(rewards) == 0 { + return []float64{}, nil + } + e.meanRewards = make([]float64, len(rewards)) for i, dist := range rewards { e.meanRewards[i] = dist.Mean() @@ -26,23 +43,42 @@ func (e *EpsilonGreedy) ComputeProbs(rewards []Dist) ([]float64, error) { } func (e EpsilonGreedy) computeProbs() []float64 { + probs := make([]float64, len(e.meanRewards)) + nonNullArms := e.numNonNullArms() + if nonNullArms == 0 { + return probs + } + maxRewardArmIndices := argsMax(e.meanRewards) numMaxima := len(maxRewardArmIndices) - numArms := len(e.meanRewards) for i := range e.meanRewards { if isIn(maxRewardArmIndices, i) { - probs[i] = (1-e.Epsilon)/float64(numMaxima) + e.Epsilon/float64(numArms) + probs[i] = (1-e.Epsilon)/float64(numMaxima) + e.Epsilon/float64(nonNullArms) } else { - probs[i] = e.Epsilon / float64(len(e.meanRewards)) + if math.IsInf(e.meanRewards[i], -1) { + probs[i] = 0 + } else { + probs[i] = e.Epsilon / float64(nonNullArms) + } } } return probs } +func (e EpsilonGreedy) numNonNullArms() int { + count := 0 + for _, val := range e.meanRewards { + if val > math.Inf(-1) { + count += 1 + } + } + return count +} + func (e EpsilonGreedy) validateEpsilon() error { if e.Epsilon < 0 || e.Epsilon > 1 { return fmt.Errorf("invalid Epsilon value: %v. Must be between 0 and 1", e.Epsilon) diff --git a/go.mod b/go.mod index 93b1eca..471429d 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,9 @@ module github.com/stitchfix/mab go 1.14 require ( - cloud.google.com/go/datastore v1.4.0 // indirect - github.com/gomodule/redigo v1.8.3 // indirect - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect - golang.org/x/tools v0.1.0 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/stretchr/testify v1.5.1 + golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 // indirect gonum.org/v1/gonum v0.8.2 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 8719880..6ba5ff5 100644 --- a/go.sum +++ b/go.sum @@ -1,442 +1,59 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.76.0 h1:Ckw+E/QYZgd/5bpI4wz4h6f+jmpvh9S9uSrKNnbicJI= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.4.0 h1:CFDJm15RpYXeEblQ0TMDUrYtqmBmbAWTy536nA8JIc8= -cloud.google.com/go/datastore v1.4.0/go.mod h1:d18825/a9bICdAIJy2EkHs9joU4RlIZ1t6l8WDdbdY0= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/gomodule/redigo v1.8.3 h1:HR0kYDX2RJZvAup8CsiJwxB4dTCSC0AaUq6S4SiLwUc= -github.com/gomodule/redigo v1.8.3/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= -github.com/gomodule/redigo/redis v0.0.0-do-not-use h1:J7XIp6Kau0WoyT4JtXHT3Ei0gA1KkSc6bc87j9v9WIo= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 h1:K+NlvTLy0oONtRtkl1jRD9xIhnItbG2PiE7YOdjPb+k= -golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2 h1:CCXrcPKiGGotvnN6jfUsKk4rRqm7q09/YbKb5xCEvtM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.34.1/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/http_reward_source.go b/http_reward_source.go new file mode 100644 index 0000000..f86d2cf --- /dev/null +++ b/http_reward_source.go @@ -0,0 +1,201 @@ +package mab + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// NewHTTPSource returns a new HTTPSource given an HttpDoer, a url for the reward service, and a RewardParser. +// Optionally provide a ContextMarshaler for encoding bandit context. +// For example, if a reward service running on localhost:1337 provides Beta reward estimates: +// client := &http.Client{timeout: time.Duration(100*time.Millisecond)} +// url := "localhost:1337/rewards" +// parser := ParseFunc(BetaFromJSON) +// marshaler := MarshalFunc(json.Marshal) +// +// source := NewHTTPSource(client, url, parser, WithContextMashaler(marshaler)) +func NewHTTPSource(client HttpDoer, url string, parser RewardParser, opts ...HTTPSourceOption) *HTTPSource { + s := &HTTPSource{ + client: client, + url: url, + parser: parser, + marshaler: MarshalFunc(json.Marshal), + } + for _, opt := range opts { + opt(s) + } + return s +} + +// HTTPSource is a basic implementation of RewardSource that gets reward estimates from an HTTP reward service. +type HTTPSource struct { + client HttpDoer + url string + parser RewardParser + marshaler ContextMarshaler +} + +// GetRewards makes a POST request to the reward URL, and parses the response into a []Dist. +// If a banditContext is provided, it will be marshaled and included in the body of the request. +func (h *HTTPSource) GetRewards(ctx context.Context, banditContext interface{}) ([]Dist, error) { + + var body io.Reader + + if banditContext != nil { + marshaled, err := h.marshaler.Marshal(banditContext) + if err != nil { + return nil, err + } + body = bytes.NewBuffer(marshaled) + } + + req, err := http.NewRequestWithContext(ctx, "POST", h.url, body) + if err != nil { + return nil, err + } + + resp, err := h.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return h.parser.Parse(respBody) +} + +// HTTPDoer is a basic interface for making HTTP requests. The net/http Client can be used or you can bring your own. +// Heimdall is a pretty good alternative client with some nice features: https://github.com/gojek/heimdall +type HttpDoer interface { + Do(*http.Request) (*http.Response, error) +} + +// RewardParser will be called to convert the response from the reward service to a slice of distributions. +type RewardParser interface { + Parse([]byte) ([]Dist, error) +} + +// ContextMarshaler is called on the banditContext and the result will become the body of the request to the bandit service. +type ContextMarshaler interface { + Marshal(banditContext interface{}) ([]byte, error) +} + +// HTTPSourceOption allows for optional arguments to NewHTTPSource +type HTTPSourceOption func(source *HTTPSource) + +// WithContextMarshaler is an optional argument to HTTPSource +func WithContextMarshaler(m ContextMarshaler) HTTPSourceOption { + return func(source *HTTPSource) { + source.marshaler = m + } +} + +// ParseFunc is an adapter to allow a normal function to be used as a RewardParser +type ParseFunc func([]byte) ([]Dist, error) + +func (p ParseFunc) Parse(b []byte) ([]Dist, error) { return p(b) } + +// MarshalFunc is an adapter to allow a normal function to be used as a ContextMarshaler +type MarshalFunc func(banditContext interface{}) ([]byte, error) + +func (m MarshalFunc) Marshal(banditContext interface{}) ([]byte, error) { return m(banditContext) } + +// BetaFromJSON converts a JSON-encoded array of Beta distributions to a []Dist. +// Expects the JSON data to be in the form: +// `[{"alpha": 123, "beta": 456}, {"alpha": 3.1415, "beta": 9.999}]` +// Returns an error if alpha or beta value are missing or less than 1 for any arm. +// Any additional keys are ignored. +func BetaFromJSON(data []byte) ([]Dist, error) { + var resp []struct { + Alpha *float64 `json:"alpha"` + Beta *float64 `json:"beta"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + result := make([]Dist, len(resp)) + + for i := range resp { + if resp[i].Alpha == nil { + return result, fmt.Errorf("missing alpha value for arm %d", i) + } + if resp[i].Beta == nil { + return result, fmt.Errorf("missing beta value for arm %d", i) + } + if *resp[i].Alpha < 1 { + return result, fmt.Errorf("arm %d alpha must be > 1. got=%f", i, *resp[i].Alpha) + } + if *resp[i].Beta < 1 { + return result, fmt.Errorf("arm %d beta must be > 1. got=%f", i, *resp[i].Beta) + } + result[i] = Beta(*resp[i].Alpha, *resp[i].Beta) + } + + return result, nil +} + +// NormalFromJSON converts a JSON-encoded array of Normal distributions to a []Dist. +// Expects the JSON data to be in the form: +// `[{"mu": 123, "sigma": 456}, {"mu": 3.1415, "sigma": 9.999}]` +// Returns an error if mu or sigma value are missing or sigma is less than 0 for any arm. +// Any additional keys are ignored. +func NormalFromJSON(data []byte) ([]Dist, error) { + var resp []struct { + Mu *float64 `json:"mu"` + Sigma *float64 `json:"sigma"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + result := make([]Dist, 0) + + for i := range resp { + if resp[i].Mu == nil { + return result, fmt.Errorf("missing mu value for arm %d", i) + } + if resp[i].Sigma == nil { + return result, fmt.Errorf("missing sigma value for arm %d", i) + } + if *resp[i].Sigma < 0 { + return result, fmt.Errorf("arm %d sigma must be > 0. got=%f", i, *resp[i].Sigma) + } + result = append(result, Normal(*resp[i].Mu, *resp[i].Sigma)) + } + + return result, nil +} + +// PointFromJSON converts a JSON-encoded array of Point distributions to a []Dist. +// Expects the JSON data to be in the form: +// `[{"mu": 123}, {"mu": 3.1415}]` +// Returns an error if mu value is missing for any arm. Any additional keys are ignored. +func PointFromJSON(data []byte) ([]Dist, error) { + var resp []struct { + Mu *float64 + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + result := make([]Dist, 0) + + for i := range resp { + if resp[i].Mu == nil { + return result, fmt.Errorf("missing mu value for arm %d", i) + } + result = append(result, Point(*resp[i].Mu)) + } + + return result, nil +} diff --git a/mab_test/epsilon_greedy_test.go b/mab_test/epsilon_greedy_test.go new file mode 100644 index 0000000..6b6bb55 --- /dev/null +++ b/mab_test/epsilon_greedy_test.go @@ -0,0 +1,250 @@ +package mab + +import ( + "testing" + + "github.com/stitchfix/mab" + "github.com/stretchr/testify/assert" +) + +func TestEpsilonGreedy_ComputeProbs(t *testing.T) { + tests := []struct { + name string + rewards []mab.Dist + epsilon float64 + expected []float64 + }{ + { + "empty", + []mab.Dist{}, + 0.1, + []float64{}, + }, + { + "single arm", + []mab.Dist{mab.Point(0.0)}, + 0.1, + []float64{1}, + }, + { + "null only", + []mab.Dist{mab.Null()}, + 0.25, + []float64{0}, + }, + { + "single point with nulls", + []mab.Dist{mab.Point(1), mab.Null(), mab.Null()}, + 0.25, + []float64{1, 0, 0}, + }, + { + "single normal", + []mab.Dist{mab.Normal(1, 5)}, + 1, + []float64{1}, + }, + { + "single normal", + []mab.Dist{mab.Normal(1, 5)}, + 1, + []float64{1}, + }, + { + "single beta", + []mab.Dist{mab.Beta(100, 150)}, + 1, + []float64{1}}, + { + "two points", + []mab.Dist{ + mab.Point(1), + mab.Point(3), + }, + 0.25, + []float64{.125, 0.875}, + }, + { + "two points with nulls", + []mab.Dist{ + mab.Point(1), + mab.Null(), + mab.Null(), + mab.Point(3), + }, + 0.25, + []float64{.125, 0, 0, 0.875}, + }, + { + "two normals", + []mab.Dist{ + mab.Normal(1, 1), + mab.Normal(3, 2), + }, + 0.25, + []float64{.125, 0.875}, + }, + { + "two normals with nulls", + []mab.Dist{ + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Normal(1, 1), + mab.Normal(3, 2), + }, + 0.25, + []float64{0, 0, 0, 0, 0.125, 0.875}, + }, + { + "two betas", + []mab.Dist{ + mab.Beta(10, 20), // mean = 1/3 + mab.Beta(10, 5), // mean = 3 + }, + 0.25, + []float64{.125, 0.875}, + }, + { + "negative reward", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + }, + 0.25, + []float64{.125, 0.875}, + }, + { + "Epsilon zero", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + mab.Point(2.5), + }, + 0, + []float64{0, 1, 0}, + }, + { + "Epsilon zero with null", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + mab.Null(), + mab.Point(2.5), + }, + 0, + []float64{0, 1, 0, 0}, + }, + { + "Epsilon one", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + mab.Point(2.5), + }, + 1, + []float64{1.0 / 3, 1.0 / 3, 1.0 / 3}, + }, + { + "Epsilon one with null", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + mab.Null(), + mab.Null(), + mab.Point(2.5), + mab.Null(), + }, + 1, + []float64{1.0 / 3, 1.0 / 3, 0, 0, 1.0 / 3, 0}, + }, + { + "multiple maxima Epsilon zero", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + mab.Point(3), + }, + 0, + []float64{0, 0.5, 0.5}, + }, + { + "multiple maxima Epsilon zero with null", + []mab.Dist{ + mab.Point(-1), + mab.Point(3), + mab.Null(), + mab.Point(3), + }, + 0, + []float64{0, 0.5, 0, 0.5}, + }, + { + "all maxima Epsilon zero", + []mab.Dist{ + mab.Point(3), + mab.Point(3), + mab.Point(3), + mab.Point(3), + }, + 0, + []float64{0.25, 0.25, 0.25, 0.25}, + }, + { + "all maxima Epsilon nonzero", + []mab.Dist{ + mab.Point(3), + mab.Point(3), + mab.Point(3), + mab.Point(3), + }, + .5, + []float64{0.25, 0.25, 0.25, 0.25}, + }, + {"two maxima Epsilon nonzero", + []mab.Dist{ + mab.Point(3), + mab.Point(3), + mab.Point(1), + mab.Point(1), + }, + .5, + []float64{0.375, 0.375, 0.125, 0.125}, + }, + {"two maxima Epsilon nonzero with null", + []mab.Dist{ + mab.Null(), + mab.Point(3), + mab.Point(3), + mab.Point(1), + mab.Point(1), + }, + .5, + []float64{0, 0.375, 0.375, 0.125, 0.125}, + }, + { + "three maxima Epsilon nonzero", + []mab.Dist{ + mab.Point(-4), + mab.Point(1), + mab.Point(1), + mab.Point(1), + }, + .1, + []float64{0.025, 0.325, 0.325, 0.325}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + strat := mab.NewEpsilonGreedy(test.epsilon) + actual, err := strat.ComputeProbs(test.rewards) + if err != nil { + t.Fatal(err) + } + if !assert.ObjectsAreEqualValues(test.expected, actual) { + t.Errorf("actual not %v, got=%v", test.expected, actual) + } + }) + } +} diff --git a/mab_test/http_reward_source_test.go b/mab_test/http_reward_source_test.go new file mode 100644 index 0000000..4f98cbc --- /dev/null +++ b/mab_test/http_reward_source_test.go @@ -0,0 +1,287 @@ +package mab + +import ( + "testing" + + "github.com/stitchfix/mab" + "github.com/stretchr/testify/assert" +) + +func TestBetaFromJSON(t *testing.T) { + tests := []struct { + name string + data []byte + expected []mab.Dist + }{ + { + "no arms", + []byte(`[]`), + []mab.Dist{}, + }, + { + "one arm", + []byte(`[{"alpha": 10, "beta": 20}]`), + []mab.Dist{mab.Beta(10, 20)}, + }, + { + "lowercase", + []byte(`[{"alpha": 10, "beta": 20}, {"alpha": 20, "beta": 10}]`), + []mab.Dist{mab.Beta(10, 20), mab.Beta(20, 10)}, + }, + { + "mixed cases", + []byte(`[{"alpha": 10, "Beta": 20}, {"Alpha": 20, "beta": 10}]`), + []mab.Dist{mab.Beta(10, 20), mab.Beta(20, 10)}, + }, + { + "floats", + []byte(`[{"alpha": 10.0, "beta": 20.12345}, {"alpha": 1.945, "beta": 10}]`), + []mab.Dist{mab.Beta(10.0, 20.12345), mab.Beta(1.945, 10)}, + }, + { + "four arms", + []byte(`[{"alpha": 10.0, "beta": 20.12345}, {"alpha": 1.945, "beta": 10}, {"alpha": 100.0, "beta": 201.2345}, {"alpha": 999.9, "beta": 3.141}]`), + []mab.Dist{mab.Beta(10.0, 20.12345), mab.Beta(1.945, 10), mab.Beta(100.0, 201.2345), mab.Beta(999.9, 3.141)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := mab.BetaFromJSON(test.data) + if err != nil { + t.Fatal(err) + } + if !assert.ObjectsAreEqualValues(test.expected, actual) { + t.Errorf("actual not %v. got=%v", test.expected, actual) + } + }) + } +} + +func TestBetaFromJSONError(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + "empty response", + []byte(``), + }, + { + "not an array", + []byte(`{"alpha": 1, "beta": 2}`), + }, + { + "missing alpha", + []byte(`[{"alpha": 11.5, "beta": 25.0}, {"beta": 49.13}]`), + }, + { + "missing beta", + []byte(`[{"alpha": 11.5}, {"alpha": 11.5, "beta": 49.13}]`), + }, + { + "wrong params", + []byte(`[{"mu": 10, "sigma": 0.25}, {"mu": 0, "sigma": 0.8}]`), + }, + { + "alpha less than one", + []byte(`[{"alpha": -4, "beta": 20}, {"alpha": 200, "beta": 100}]`), + }, + { + "beta less than one", + []byte(`[{"alpha": 40, "beta": -0.1}, {"alpha": 200, "beta": 100}]`), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := mab.BetaFromJSON(test.data) + if err == nil { + t.Error("expected error but didn't get one") + } + }) + } +} + +func TestNormalFromJSON(t *testing.T) { + tests := []struct { + name string + data []byte + expected []mab.Dist + }{ + { + "no arms", + []byte(`[]`), + []mab.Dist{}, + }, + { + "one arm", + []byte(`[{"mu": 10, "sigma": 20}]`), + []mab.Dist{mab.Normal(10, 20)}, + }, + { + "lowercase", + []byte(`[{"mu": 10, "sigma": 20}, {"mu": 20, "sigma": 10}]`), + []mab.Dist{mab.Normal(10, 20), mab.Normal(20, 10)}, + }, + { + "mixed cases", + []byte(`[{"mu": 10, "Sigma": 20}, {"Mu": 20, "sigma": 10}]`), + []mab.Dist{mab.Normal(10, 20), mab.Normal(20, 10)}, + }, + { + "floats", + []byte(`[{"mu": 10.0, "sigma": 20.12345}, {"mu": 1.945, "sigma": 10}]`), + []mab.Dist{mab.Normal(10.0, 20.12345), mab.Normal(1.945, 10)}, + }, + { + "negative mu", + []byte(`[{"mu": -10.0, "sigma": 20.12345}, {"mu": -1.945, "sigma": 10}]`), + []mab.Dist{mab.Normal(-10.0, 20.12345), mab.Normal(-1.945, 10)}, + }, + { + "four arms", + []byte(`[{"mu": 10.0, "sigma": 20.12345}, {"mu": 1.945, "sigma": 10}, {"mu": 100.0, "sigma": 201.2345}, {"mu": 999.9, "sigma": 3.141}]`), + []mab.Dist{mab.Normal(10.0, 20.12345), mab.Normal(1.945, 10), mab.Normal(100.0, 201.2345), mab.Normal(999.9, 3.141)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := mab.NormalFromJSON(test.data) + if err != nil { + t.Fatal(err) + } + if !assert.ObjectsAreEqualValues(test.expected, actual) { + t.Errorf("actual not %v. got=%v", test.expected, actual) + } + }) + } +} + +func TestNormalFromJSONError(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + "empty response", + []byte(``), + }, + { + "not an array", + []byte(`{"mu": 1, "sigma": 2}`), + }, + { + "missing mu", + []byte(`[{"mu": 11.5, "sigma": 25.0}, {"sigma": 49.13}]`), + }, + { + "missing sigma", + []byte(`[{"mu": 11.5}, {"mu": 11.5, "sigma": 49.13}]`), + }, + { + "wrong params", + []byte(`[{"alpha": 10, "beta": 0.25}, {"alpha": 0, "beta": 0.8}]`), + }, + { + "sigma less than one", + []byte(`[{"mu": -4, "sigma": 20}, {"mu": 200, "sigma": -100}]`), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := mab.NormalFromJSON(test.data) + if err == nil { + t.Error("expected error but didn't get one") + } + }) + } +} + +func TestPointFromJSON(t *testing.T) { + tests := []struct { + name string + data []byte + expected []mab.Dist + }{ + { + "no arms", + []byte(`[]`), + []mab.Dist{}, + }, + { + "one arm", + []byte(`[{"mu": 10}]`), + []mab.Dist{mab.Point(10)}, + }, + { + "lowercase", + []byte(`[{"mu": 10}, {"mu": 20}]`), + []mab.Dist{mab.Point(10), mab.Point(20)}, + }, + { + "mixed cases", + []byte(`[{"mu": 10}, {"Mu": 20}]`), + []mab.Dist{mab.Point(10), mab.Point(20)}, + }, + { + "floats", + []byte(`[{"mu": 10.0}, {"mu": 1.945}]`), + []mab.Dist{mab.Point(10.0), mab.Point(1.945)}, + }, + { + "negative mu", + []byte(`[{"mu": -10.0}, {"mu": -1.945, "sigma": 10}]`), + []mab.Dist{mab.Point(-10.0), mab.Point(-1.945)}, + }, + { + "four arms", + []byte(`[{"mu": 10.0}, {"mu": 1.945}, {"mu": 100.0}, {"mu": -999.9}]`), + []mab.Dist{mab.Point(10.0), mab.Point(1.945), mab.Point(100.0), mab.Point(-999.9)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual, err := mab.PointFromJSON(test.data) + if err != nil { + t.Fatal(err) + } + if !assert.ObjectsAreEqualValues(test.expected, actual) { + t.Errorf("actual not %v. got=%v", test.expected, actual) + } + }) + } +} + +func TestPointFromJSONError(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + "empty response", + []byte(``), + }, + { + "not an array", + []byte(`{"mu": 1}`), + }, + { + "missing mu", + []byte(`[{"mu": 11.5}, {}]`), + }, + { + "wrong params", + []byte(`[{"alpha": 10, "beta": 0.25}, {"alpha": 0, "beta": 0.8}]`), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := mab.PointFromJSON(test.data) + if err == nil { + t.Error("expected error but didn't get one") + } + }) + } +} diff --git a/mab_test/mab_test.go b/mab_test/mab_test.go index 495dd75..eeb94fc 100644 --- a/mab_test/mab_test.go +++ b/mab_test/mab_test.go @@ -25,7 +25,7 @@ func TestThompson_SelectArm(t *testing.T) { Sampler: mab.NewSha1Sampler(), } - result, err := b.SelectArm(context.Background(), "12345") + result, err := b.SelectArm(context.Background(), "12345", nil) if err != nil { t.Fatal(err) } @@ -37,3 +37,28 @@ func TestThompson_SelectArm(t *testing.T) { t.Errorf("result not %d, got=%d", expected, actual) } } + +func TestEpsilon_SelectArm(t *testing.T) { + rewards := map[string][]mab.Dist{ + "blue": {mab.Point(-0.5), mab.Point(0.5)}, + "red": {mab.Point(0.5), mab.Point(-0.5)}, + } + + b := mab.Bandit{ + RewardSource: &mab.ContextualRewardStub{Rewards: rewards}, + Strategy: &mab.EpsilonGreedy{Epsilon: 0.0}, + Sampler: mab.NewSha1Sampler(), + } + + result, err := b.SelectArm(context.Background(), "12345", "red") + if err != nil { + t.Fatal(err) + } + + actual := result.Arm + expected := 0 + + if actual != expected { + t.Errorf("result not %d, got=%d", expected, actual) + } +} diff --git a/mab_test/proportional_test.go b/mab_test/proportional_test.go new file mode 100644 index 0000000..79d6e53 --- /dev/null +++ b/mab_test/proportional_test.go @@ -0,0 +1,109 @@ +package mab + +import ( + "testing" + + "github.com/stitchfix/mab" + "github.com/stretchr/testify/assert" +) + +func TestProportional_ComputeProbs(t *testing.T) { + tests := []struct { + name string + rewards []mab.Dist + expected []float64 + }{ + { + "empty", + []mab.Dist{}, + []float64{}, + }, + { + "one arm", + []mab.Dist{mab.Point(1)}, + []float64{1}, + }, + { + "null only", + []mab.Dist{mab.Null()}, + []float64{0}, + }, + { + "single point with nulls", + []mab.Dist{mab.Point(1), mab.Null(), mab.Null()}, + []float64{1, 0, 0}, + }, + { + "single normal", + []mab.Dist{mab.Normal(1, 5)}, + []float64{1}, + }, + { + "single normal", + []mab.Dist{mab.Normal(1, 5)}, + []float64{1}, + }, + { + "single beta", + []mab.Dist{mab.Beta(100, 150)}, + []float64{1}}, + { + "two points", + []mab.Dist{ + mab.Point(1), + mab.Point(3), + }, + []float64{.25, .75}, + }, + { + "two points with nulls", + []mab.Dist{ + mab.Point(1), + mab.Null(), + mab.Null(), + mab.Point(3), + }, + []float64{.25, 0, 0, 0.75}, + }, + { + "two normals", + []mab.Dist{ + mab.Normal(1, 1), + mab.Normal(3, 2), + }, + []float64{.25, 0.75}, + }, + { + "two normals with nulls", + []mab.Dist{ + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Normal(1, 1), + mab.Normal(3, 2), + }, + []float64{0, 0, 0, 0, 0.25, 0.75}, + }, + { + "two betas", + []mab.Dist{ + mab.Beta(10, 20), // mean = 1/3 + mab.Beta(10, 5), // mean = 2/3 + }, + []float64{1. / 3, 2. / 3}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + strat := mab.NewProportional() + actual, err := strat.ComputeProbs(test.rewards) + if err != nil { + t.Fatal(err) + } + if !assert.ObjectsAreEqualValues(test.expected, actual) { + t.Errorf("actual not %v, got=%v", test.expected, actual) + } + }) + } +} diff --git a/mab_test/sha1sampler_test.go b/mab_test/sha1sampler_test.go new file mode 100644 index 0000000..55124ce --- /dev/null +++ b/mab_test/sha1sampler_test.go @@ -0,0 +1,115 @@ +package mab + +import ( + "strconv" + "testing" + + "github.com/stitchfix/mab" + "gonum.org/v1/gonum/stat/distuv" +) + +func TestSha1Sampler_Sample(t *testing.T) { + + tests := []struct { + name string + weights []float64 + }{ + { + "one weight", + []float64{1}, + }, + { + "equal weights", + []float64{1.0, 1.0}, + }, + { + "unequal weights", + []float64{2, 1}, + }, + { + "odd number of weights", + []float64{1, 1, 1}, + }, + { + "high and low probabilities", + []float64{0.001, 0.999}, + }, + } + + for _, test := range tests { + + var samples []int + counts := make(map[int]float64) + + numIter := 10_000 + + t.Run(test.name, func(t *testing.T) { + s := mab.NewSha1Sampler() + for i := 0; i < numIter; i++ { + sample, err := s.Sample(test.weights, strconv.Itoa(i)) + if err != nil { + t.Fatal(err) + } + samples = append(samples, sample) + counts[sample] += 1 + } + + if len(test.weights) == 1 { + testSingleWeight(t, samples) + return + } + + // for non-trivial examples we can do a statistical test on the frequencies + testFrequencies(t, numIter, test.weights, counts) + }) + } +} + +func testSingleWeight(t *testing.T, samples []int) { + for i, sample := range samples { + if sample != 0 { + t.Errorf("sample %d not 0. got=%v", i, sample) + } + } +} + +func testFrequencies(t *testing.T, numIter int, weights []float64, observed map[int]float64) { + if len(observed) != len(weights) { + t.Fatalf("len(observed) != len(weights): %v, %v", observed, weights) + } + + sumW := 0.0 + for _, w := range weights { + sumW += w + } + + expected := make(map[int]float64) + for i, w := range weights { + expected[i] = w * float64(numIter) / sumW + } + + if len(observed) != len(expected) { + t.Fatalf("len(observed) != len(expected): %v, %v", observed, expected) + } + + chi2 := 0.0 + + for val, obsCount := range observed { + expCount, ok := expected[val] + if !ok { + t.Fatalf("missing expected fraction for value: %d. got=%v", val, expected) + } + + chi2 += (obsCount - expCount) * (obsCount - expCount) / expCount + } + + dof := float64(len(observed) - 1) + dist := distuv.ChiSquared{K: dof} + + pVal := 1 - dist.CDF(chi2) + alpha := 0.0001 + + if pVal <= alpha { + t.Errorf("expected frequencies %v, got=%v [pVal=%v]", expected, observed, pVal) + } +} diff --git a/mab_test/thompson_mc_test.go b/mab_test/thompson_mc_test.go new file mode 100644 index 0000000..09b5e4a --- /dev/null +++ b/mab_test/thompson_mc_test.go @@ -0,0 +1,26 @@ +package mab + +import ( + "fmt" + + "github.com/stitchfix/mab" +) + +func ExampleThompsonMC_ComputeProbs() { + t := mab.NewThompsonMC(1E6) + rewards := []mab.Dist{ + mab.Beta(1989, 21290), + mab.Beta(40, 474), + mab.Beta(64, 730), + mab.Beta(71, 818), + mab.Beta(52, 659), + mab.Beta(59, 718), + } + probs, err := t.ComputeProbs(rewards) + if err != nil { + panic(err) + } + + fmt.Printf("%.4f", probs) + // Output: [0.2967 0.1762 0.2033 0.1687 0.0613 0.0938] +} diff --git a/mab_test/thompson_test.go b/mab_test/thompson_test.go new file mode 100644 index 0000000..48eb94c --- /dev/null +++ b/mab_test/thompson_test.go @@ -0,0 +1,251 @@ +package mab + +import ( + "fmt" + "math" + "testing" + + "github.com/stitchfix/mab" + "github.com/stitchfix/mab/numint" +) + +func ExampleThompson_ComputeProbs() { + strat := mab.NewThompson(numint.NewQuadrature()) + rewards := []mab.Dist{ + mab.Beta(1989, 21290), + mab.Beta(40, 474), + mab.Beta(64, 730), + mab.Beta(71, 818), + mab.Beta(52, 659), + mab.Beta(59, 718), + } + probs, err := strat.ComputeProbs(rewards) + if err != nil { + panic(err) + } + + fmt.Printf("%.4f", probs) + // Output: [0.2963 0.1760 0.2034 0.1690 0.0614 0.0939] +} + +func TestThompson_ComputeProbs(t *testing.T) { + tests := []struct { + name string + rewards []mab.Dist + expected []float64 + }{ + { + "nil", + nil, + make([]float64, 0), + }, + { + "empty", + make([]mab.Dist, 0), + make([]float64, 0), + }, + { + "single arm", + []mab.Dist{mab.Normal(0, 1.0)}, + []float64{1}, + }, + { + "equal arms", + []mab.Dist{mab.Normal(0, 1.0), mab.Normal(0, 1.0)}, + []float64{0.5, 0.5}, + }, + { + "one null", + []mab.Dist{mab.Null()}, + []float64{0}, + }, + { + "several nulls", + []mab.Dist{mab.Null(), mab.Null(), mab.Null()}, + []float64{0, 0, 0}, + }, + { + "one non-null several nulls", + []mab.Dist{mab.Null(), mab.Null(), mab.Beta(10, 20), mab.Null()}, + []float64{0, 0, 1, 0}, + }, + { + "normals", + []mab.Dist{ + mab.Normal(1, 0.5), + mab.Normal(0.8, 0.44), + mab.Normal(2, 4.5), + mab.Normal(-1.5, 0.8), + mab.Normal(0, 0.8), + mab.Normal(4, 0.01), + }, + []float64{0, 0, 0.32832939702916675, 0, 0, 0.6715962238578759}, + }, + { + "normals with nulls", + []mab.Dist{ + mab.Normal(1, 0.5), + mab.Normal(0.8, 0.44), + mab.Null(), + mab.Normal(2, 4.5), + mab.Normal(-1.5, 0.8), + mab.Normal(0, 0.8), + mab.Normal(4, 0.01), + mab.Null(), + }, + []float64{0, 0, 0, 0.32832939702916675, 0, 0, 0.6715962238578759, 0}, + }, + { + "betas", + []mab.Dist{ + mab.Beta(100, 50), + mab.Beta(30, 100), + mab.Beta(5, 5), + mab.Beta(10, 5), + mab.Beta(20, 200), + }, + []float64{0.413633, 0, 0.098703, 0.487664, 0}, + }, + { + "betas with null", + []mab.Dist{ + mab.Null(), + mab.Beta(100, 50), + mab.Beta(30, 100), + mab.Beta(5, 5), + mab.Beta(10, 5), + mab.Beta(20, 200), + }, + []float64{0, 0.413633, 0, 0.098703, 0.487664, 0}, + }, + { + "lots of nulls", + []mab.Dist{ + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Beta(100, 100), + }, + []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + }, + { + "lots of nulls", + []mab.Dist{ + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Beta(30, 20), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Null(), + mab.Beta(300, 300), + }, + []float64{0, 0, 0, 0, 0, 0, 0, 0, 0.915955668704697, 0, 0, 0, 0, 0, 0, 0.08442959550732976}, + }, + { + "spicy betas", + []mab.Dist{ + mab.Beta(1988.9969421012, 21290.29165727936), + mab.Beta(50.513724206539536, 694.8915442828242), + mab.Beta(40.22907217881993, 474.05635888115313), + mab.Beta(63.51183105653544, 727.0899538364148), + mab.Beta(31.261111088044935, 411.1179082444311), + mab.Beta(21.92459706142498, 357.99764835992886), + mab.Beta(71.24351745432674, 818.4214002728952), + mab.Beta(52.28986733645648, 659.2207151426613), + mab.Beta(58.626012977120325, 718.5085688230059), + mab.Beta(27.76180147538136, 391.16613861489384), + }, + []float64{ + 0.23448743303613864, + 0.015318543048527354, + 0.17017806247696898, + 0.17666880201042032, + 0.06656095618639102, + 0.008850309350875189, + 0.15737618680298987, + 0.058618352704498694, + 0.07651307073837736, + 0.035421658908035586, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ts := mab.NewThompson(numint.NewQuadrature()) + actual, err := ts.ComputeProbs(test.rewards) + if err != nil { + t.Fatal(err) + } + if !closeEnough(test.expected, actual) { + t.Errorf("actual not %v, got=%v", test.expected, actual) + } + }) + } +} + +func BenchmarkThompson_ComputeProbs(b *testing.B) { + rewards := []mab.Dist{ + mab.Beta(1988.9969421012, 21290.29165727936), + mab.Beta(50.513724206539536, 694.8915442828242), + mab.Beta(40.22907217881993, 474.05635888115313), + mab.Beta(63.51183105653544, 727.0899538364148), + mab.Beta(31.261111088044935, 411.1179082444311), + mab.Beta(21.92459706142498, 357.99764835992886), + mab.Beta(71.24351745432674, 818.4214002728952), + mab.Beta(52.28986733645648, 659.2207151426613), + mab.Beta(58.626012977120325, 718.5085688230059), + mab.Beta(27.76180147538136, 391.16613861489384), + } + startTol := 0.1 + endTol := 0.001 + for tol := startTol; tol >= endTol; tol /= 10 { + strat := mab.NewThompson(numint.NewQuadrature(numint.WithAbsAndRelTol(tol, tol))) + b.Run(fmt.Sprintf("tolerance_%v", tol), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := strat.ComputeProbs(rewards) + if err != nil { + b.Error(err) + } + } + }) + } + +} + +func closeEnough(a, b []float64) bool { + tolerance := 0.001 + + if len(a) != len(b) { + return false + } + + for i := range a { + diff := math.Abs(a[i] - b[i]) + if diff > tolerance { + return false + } + } + return true +} diff --git a/numint/quadrature.go b/numint/quadrature.go index 638dcff..35f9a21 100644 --- a/numint/quadrature.go +++ b/numint/quadrature.go @@ -55,6 +55,9 @@ type Quadrature struct { } func (q Quadrature) Integrate(f func(float64) float64, a float64, b float64) (float64, error) { + if a == b { + return 0, nil + } if !q.canConverge() { return math.NaN(), errors.New("integral cannot converge. check tolerance") } diff --git a/proportional.go b/proportional.go index 8b991b6..502774a 100644 --- a/proportional.go +++ b/proportional.go @@ -1,21 +1,38 @@ package mab -import "fmt" +import ( + "fmt" + "math" +) +func NewProportional() *Proportional { + return &Proportional{} +} + +// Proportional is a trivial bandit strategy that returns arm-selection probabilities proportional to the mean reward estimate for each arm. +// This can be used when a bandit service wants to provide selection weights rather than reward estimates. +// Proportional treats Point(0) and Null() the same way, assigning them zero selection probability. type Proportional struct { meanRewards, probs []float64 } +// ComputeProbs computes probabilities proportional to the mean reward of each arm. +// Returns an error if any arm has a negative finite mean reward. +// A mean reward of negative infinity is treated as zero, so that a Null() distribution is treated the same as Point(0). func (p *Proportional) ComputeProbs(rewards []Dist) ([]float64, error) { p.meanRewards = make([]float64, len(rewards)) for i, dist := range rewards { mean := dist.Mean() - if mean < 0 { + + switch { + default: + p.meanRewards[i] = mean + case mean > math.Inf(-1) && mean < 0: return nil, fmt.Errorf("negative mean reward") + case math.IsInf(mean, -1): // indicates a Null distribution + p.meanRewards[i] = 0 } - - p.meanRewards[i] = mean } return p.computeProbs() @@ -31,6 +48,11 @@ func (p Proportional) computeProbs() ([]float64, error) { } p.probs = make([]float64, len(p.meanRewards)) + + if norm == 0 { + return p.probs, nil + } + for i, mean := range p.meanRewards { p.probs[i] = mean / norm } diff --git a/reward_source.go b/reward_source.go deleted file mode 100644 index a63f426..0000000 --- a/reward_source.go +++ /dev/null @@ -1,13 +0,0 @@ -package mab - -import ( - "context" -) - -type RewardStub struct { - Rewards []Dist -} - -func (s *RewardStub) GetRewards(context.Context) ([]Dist, error) { - return s.Rewards, nil -} diff --git a/reward_stub.go b/reward_stub.go new file mode 100644 index 0000000..20b2674 --- /dev/null +++ b/reward_stub.go @@ -0,0 +1,38 @@ +package mab + +import ( + "context" + "fmt" +) + +// RewardStub is a static non-contextual RewardSource that can be used for testing and development. +type RewardStub struct { + Rewards []Dist +} + +// GetRewards gets the static rewards +func (s *RewardStub) GetRewards(context.Context, interface{}) ([]Dist, error) { + return s.Rewards, nil +} + +// ContextualRewardStub is a static contextual RewardSource that can be used for testing and development of contextual bandits. +// It assumes that the context can be specified with a string. +type ContextualRewardStub struct { + Rewards map[string][]Dist +} + +// GetRewards gets the static rewards for a given banditContext string. +func (c *ContextualRewardStub) GetRewards(ctx context.Context, banditContext interface{}) ([]Dist, error) { + key, ok := banditContext.(string) + if !ok { + return nil, fmt.Errorf("banditContext must be a string") + } + + val, ok := c.Rewards[key] + + if !ok { + return nil, fmt.Errorf("no distributions for %s", val) + } + + return val, nil +} diff --git a/sha1sampler.go b/sha1sampler.go index 22c5932..546e903 100644 --- a/sha1sampler.go +++ b/sha1sampler.go @@ -14,10 +14,13 @@ func NewSha1Sampler() *Sha1Sampler { } } +// Sha1Sampler is a Sampler that uses the SHA1 hash of input unit to select an arm index with probability proportional to some given weights. type Sha1Sampler struct { numBuckets int } +// Sample returns the selected arm for a given set of weights and input unit. +// An error is returned if any negative weight is encountered. func (s *Sha1Sampler) Sample(weights []float64, unit string) (int, error) { checkSum := sha1.Sum([]byte(unit)) @@ -49,11 +52,14 @@ func (s *Sha1Sampler) getIndex(weights []float64, bucket int) (int, error) { curBucket := -1.0 for i, w := range weights { + if w < 0 { + return -1, fmt.Errorf("negative weight") + } curBucket += w * float64(s.numBuckets) / sumWeights if curBucket >= float64(bucket) { return i, nil } } - return -1, fmt.Errorf("bucket out of range") + return -1, fmt.Errorf("bucket out of range") // this code should be unreachable } diff --git a/sha1sampler_internal_test.go b/sha1sampler_internal_test.go index 5d29ec6..0c9ac4f 100644 --- a/sha1sampler_internal_test.go +++ b/sha1sampler_internal_test.go @@ -69,9 +69,39 @@ func TestSha1Sampler_getIndex(t *testing.T) { 999, 2, }, + { + "zero weights", + []float64{0, 1, 0}, + 0, + 1, + }, + { + "zero weights", + []float64{0, 1, 0}, + 500, + 1, + }, + { + "zero weights", + []float64{0, 1, 0}, + 999, + 1, + }, + { + "zero weights", + []float64{0, 1, 1}, + 499, + 1, + }, + { + "zero weights", + []float64{0, 1, 1}, + 500, + 2, + }, } - s := Sha1Sampler{1000} + s := NewSha1Sampler() for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/thompson.go b/thompson.go index 1fa55c9..17b2101 100644 --- a/thompson.go +++ b/thompson.go @@ -17,6 +17,10 @@ type Integrator interface { } func (t *Thompson) ComputeProbs(rewards []Dist) ([]float64, error) { + if len(rewards) == 0 { + return []float64{}, nil + } + t.rewards = rewards return t.computeProbs() } diff --git a/thompson_mc.go b/thompson_mc.go index 905e447..01bf36d 100644 --- a/thompson_mc.go +++ b/thompson_mc.go @@ -1,17 +1,23 @@ package mab +// NewThompsonMC returns a new ThompsonMC with numIterations. func NewThompsonMC(numIterations int) *ThompsonMC { return &ThompsonMC{ NumIterations: numIterations, } } +// ThompsonMC is a Monte-Carlo based implementation of Thompson sampling Strategy. +// It should not be used in production but is provided only as an example and for comparison with the Thompson Strategy, +// which is much faster and more accurate. type ThompsonMC struct { NumIterations int rewards []Dist counts, samples []float64 } +// ComputeProbs estimates the arm-selection probabilities by repeatedly sampling from the Dist for each arm, +// and counting how many times each arm yields the maximal sampled value. func (t *ThompsonMC) ComputeProbs(rewards []Dist) ([]float64, error) { t.rewards = rewards return t.computeProbs(), nil diff --git a/thompson_mc_test.go b/thompson_mc_test.go deleted file mode 100644 index faf35de..0000000 --- a/thompson_mc_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package mab - -import ( - "fmt" -) - -func ExampleThompsonMC_Probabilities() { - t := NewThompsonMC(1E6) - rewards := []Dist{ - Beta(1989, 21290), - Beta(40, 474), - Beta(64, 730), - Beta(71, 818), - Beta(52, 659), - Beta(59, 718), - } - probs, err := t.ComputeProbs(rewards) - if err != nil { - panic(err) - } - - fmt.Printf("%.4f", probs) - // Output: [0.2967 0.1762 0.2033 0.1687 0.0613 0.0938] -} diff --git a/thompson_test.go b/thompson_test.go deleted file mode 100644 index 02ca419..0000000 --- a/thompson_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package mab - -import ( - "fmt" - - "github.com/stitchfix/mab/numint" -) - -func ExampleThompson_Probabilities() { - t := NewThompson(numint.NewQuadrature()) - rewards := []Dist{ - Beta(1989, 21290), - Beta(40, 474), - Beta(64, 730), - Beta(71, 818), - Beta(52, 659), - Beta(59, 718), - } - probs, err := t.ComputeProbs(rewards) - if err != nil { - panic(err) - } - - fmt.Printf("%.4f", probs) - // Output: [0.2963 0.1760 0.2034 0.1690 0.0614 0.0939] -}