-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from kgrygiel/master
Base Recommender model classes: Histogram and CircularBuffer.
- Loading branch information
Showing
6 changed files
with
563 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package util | ||
|
||
// Histogram represents an approximate distribution of some variable. | ||
type Histogram interface { | ||
// Returns an approximation of the given percentile of the distribution. | ||
// Note: the argument passed to Percentile() is a number between | ||
// 0 and 1. For example 0.5 corresponds to the median and 0.9 to the | ||
// 90th percentile. | ||
// If the histogram is empty, Percentile() returns 0.0. | ||
Percentile(percentile float64) float64 | ||
|
||
// Add a sample with a given value and weight. | ||
AddSample(value float64, weight float64) | ||
|
||
// Remove a sample with a given value and weight. Note that the total | ||
// weight of samples with a given value cannot be negative. | ||
SubtractSample(value float64, weight float64) | ||
|
||
// Returns true if the histogram is empty. | ||
IsEmpty() bool | ||
} | ||
|
||
// NewHistogram returns a new Histogram instance using given options. | ||
func NewHistogram(options HistogramOptions) Histogram { | ||
return &histogram{ | ||
&options, make([]float64, options.NumBuckets()), 0.0, | ||
options.NumBuckets() - 1, 0} | ||
} | ||
|
||
// Simple bucket-based implementation of the Histogram interface. Each bucket | ||
// holds the total weight of samples that belong to it. | ||
// Percentile() returns the middle of the correspodning bucket. | ||
// Resolution (bucket boundaries) of the histogram depends on the options. | ||
// There's no interpolation within buckets (i.e. one sample falls to exactly one | ||
// bucket). | ||
// A bucket is considered empty if its weight is smaller than options.Epsilon(). | ||
type histogram struct { | ||
// Bucketing scheme. | ||
options *HistogramOptions | ||
// Cumulative weight of samples in each bucket. | ||
bucketWeight []float64 | ||
// Total cumulative weight of samples in all buckets. | ||
totalWeight float64 | ||
// Index of the first non-empty bucket if there's any. Otherwise index | ||
// of the last bucket. | ||
minBucket int | ||
// Index of the last non-empty bucket if there's any. Otherwise 0. | ||
maxBucket int | ||
} | ||
|
||
func (h *histogram) AddSample(value float64, weight float64) { | ||
if weight < 0.0 { | ||
panic("sample weight must be non-negative") | ||
} | ||
bucket := (*h.options).FindBucket(value) | ||
h.bucketWeight[bucket] += weight | ||
h.totalWeight += weight | ||
if bucket < h.minBucket { | ||
h.minBucket = bucket | ||
} | ||
if bucket > h.maxBucket { | ||
h.maxBucket = bucket | ||
} | ||
} | ||
func (h *histogram) SubtractSample(value float64, weight float64) { | ||
if weight < 0.0 { | ||
panic("sample weight must be non-negative") | ||
} | ||
bucket := (*h.options).FindBucket(value) | ||
epsilon := (*h.options).Epsilon() | ||
if weight > h.bucketWeight[bucket]-epsilon { | ||
weight = h.bucketWeight[bucket] | ||
} | ||
h.totalWeight -= weight | ||
h.bucketWeight[bucket] -= weight | ||
lastBucket := (*h.options).NumBuckets() - 1 | ||
for h.bucketWeight[h.minBucket] < epsilon && h.minBucket < lastBucket { | ||
h.minBucket++ | ||
} | ||
for h.bucketWeight[h.maxBucket] < epsilon && h.maxBucket > 0 { | ||
h.maxBucket-- | ||
} | ||
} | ||
|
||
func (h *histogram) Percentile(percentile float64) float64 { | ||
if h.IsEmpty() { | ||
return 0.0 | ||
} | ||
partialSum := 0.0 | ||
threshold := percentile * h.totalWeight | ||
bucket := h.minBucket | ||
for ; bucket < h.maxBucket; bucket++ { | ||
partialSum += h.bucketWeight[bucket] | ||
if partialSum >= threshold { | ||
break | ||
} | ||
} | ||
bucketStart := (*h.options).GetBucketStart(bucket) | ||
if bucket < (*h.options).NumBuckets()-1 { | ||
// Return the middle point between the bucket boundaries. | ||
bucketEnd := (*h.options).GetBucketStart(bucket + 1) | ||
return (bucketStart + bucketEnd) / 2.0 | ||
} | ||
// Return the start of the last bucket (note that the last bucket | ||
// doesn't have an upper bound). | ||
return bucketStart | ||
} | ||
|
||
func (h *histogram) IsEmpty() bool { | ||
return h.bucketWeight[h.minBucket] < (*h.options).Epsilon() | ||
} |
135 changes: 135 additions & 0 deletions
135
vertical-pod-autoscaler/recommender/util/histogram_options.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package util | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"math" | ||
) | ||
|
||
// HistogramOptions define the number and size of buckets of a histogram. | ||
type HistogramOptions interface { | ||
// Returns the number of buckets in the histogram. | ||
NumBuckets() int | ||
// Returns the index of the bucket to which the given value falls. | ||
// If the value is outside of the range covered by the histogram, it | ||
// returns the closest bucket (either the first or the last one). | ||
FindBucket(value float64) int | ||
// Returns the start of the bucket with a given index. If the index is | ||
// outside the [0..NumBuckets() - 1] range, the result is undefined. | ||
GetBucketStart(bucket int) float64 | ||
// Returns the minimum weight for a bucket to be considered non-empty. | ||
Epsilon() float64 | ||
} | ||
|
||
// NewLinearHistogramOptions returns HistogramOptions describing a histogram | ||
// with a given number of fixed-size buckets, with the first bucket start at 0.0 | ||
// and the last bucket start larger or equal to maxValue. | ||
// Requires maxValue > 0, bucketSize > 0, epsilon > 0. | ||
func NewLinearHistogramOptions( | ||
maxValue float64, bucketSize float64, epsilon float64) (HistogramOptions, error) { | ||
if maxValue <= 0.0 || bucketSize <= 0.0 || epsilon <= 0.0 { | ||
return nil, errors.New("maxValue and bucketSize must both be positive") | ||
} | ||
numBuckets := int(math.Ceil(maxValue/bucketSize)) + 1 | ||
return &linearHistogramOptions{numBuckets, bucketSize, epsilon}, nil | ||
} | ||
|
||
// NewExponentialHistogramOptions returns HistogramOptions describing a | ||
// histogram with exponentially growing bucket boundaries. The first bucket | ||
// covers the range [0..firstBucketSize). Consecutive buckets are of the form | ||
// [x(n)..x(n) * ratio) for n = 1 .. numBuckets - 1. | ||
// The last bucket start is larger or equal to maxValue. | ||
// Requires maxValue > 0, firstBucketSize > 0, ratio > 1, epsilon > 0. | ||
func NewExponentialHistogramOptions( | ||
maxValue float64, firstBucketSize float64, ratio float64, epsilon float64) (HistogramOptions, error) { | ||
if maxValue <= 0.0 || firstBucketSize <= 0.0 || ratio <= 1.0 || epsilon <= 0.0 { | ||
return nil, errors.New( | ||
"maxValue, firstBucketSize and epsilon must be > 0.0, ratio must be > 1.0") | ||
} | ||
numBuckets := int(math.Ceil(math.Log(maxValue/firstBucketSize)/math.Log(ratio))) + 2 | ||
return &exponentialHistogramOptions{numBuckets, firstBucketSize, ratio, epsilon}, nil | ||
} | ||
|
||
type linearHistogramOptions struct { | ||
numBuckets int | ||
bucketSize float64 | ||
epsilon float64 | ||
} | ||
|
||
type exponentialHistogramOptions struct { | ||
numBuckets int | ||
firstBucketSize float64 | ||
ratio float64 | ||
epsilon float64 | ||
} | ||
|
||
func (o *linearHistogramOptions) NumBuckets() int { | ||
return o.numBuckets | ||
} | ||
|
||
func (o *linearHistogramOptions) FindBucket(value float64) int { | ||
bucket := int(value / o.bucketSize) | ||
if bucket < 0 { | ||
return 0 | ||
} | ||
if bucket >= o.numBuckets { | ||
return o.numBuckets - 1 | ||
} | ||
return bucket | ||
} | ||
|
||
func (o *linearHistogramOptions) GetBucketStart(bucket int) float64 { | ||
if bucket < 0 || bucket >= o.numBuckets { | ||
panic(fmt.Sprintf("index %d out of range [0..%d]", bucket, o.numBuckets-1)) | ||
} | ||
return float64(bucket) * o.bucketSize | ||
} | ||
|
||
func (o *linearHistogramOptions) Epsilon() float64 { | ||
return o.epsilon | ||
} | ||
|
||
func (o *exponentialHistogramOptions) NumBuckets() int { | ||
return o.numBuckets | ||
} | ||
|
||
func (o *exponentialHistogramOptions) FindBucket(value float64) int { | ||
if value < o.firstBucketSize { | ||
return 0 | ||
} | ||
bucket := int(math.Log(value/o.firstBucketSize)/math.Log(o.ratio)) + 1 | ||
if bucket >= o.numBuckets { | ||
return o.numBuckets - 1 | ||
} | ||
return bucket | ||
} | ||
|
||
func (o *exponentialHistogramOptions) GetBucketStart(bucket int) float64 { | ||
if bucket < 0 || bucket >= o.numBuckets { | ||
panic(fmt.Sprintf("index %d out of range [0..%d]", bucket, o.numBuckets-1)) | ||
} | ||
if bucket == 0 { | ||
return 0.0 | ||
} | ||
return o.firstBucketSize * math.Pow(o.ratio, float64(bucket-1)) | ||
} | ||
|
||
func (o *exponentialHistogramOptions) Epsilon() float64 { | ||
return o.epsilon | ||
} |
64 changes: 64 additions & 0 deletions
64
vertical-pod-autoscaler/recommender/util/histogram_options_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package util | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
var ( | ||
epsilon = 0.001 | ||
) | ||
|
||
// Test all methods of LinearHistogramOptions using a sample bucketing scheme. | ||
func TestLinearHistogramOptions(t *testing.T) { | ||
o, err := NewLinearHistogramOptions(5.0, 0.3, epsilon) | ||
assert.Nil(t, err) | ||
assert.Equal(t, epsilon, o.Epsilon()) | ||
assert.Equal(t, 18, o.NumBuckets()) | ||
|
||
assert.Equal(t, 0.0, o.GetBucketStart(0)) | ||
assert.Equal(t, 5.1, o.GetBucketStart(17)) | ||
|
||
assert.Equal(t, 0, o.FindBucket(-1.0)) | ||
assert.Equal(t, 0, o.FindBucket(0.0)) | ||
assert.Equal(t, 4, o.FindBucket(1.3)) | ||
assert.Equal(t, 17, o.FindBucket(100.0)) | ||
} | ||
|
||
// Test all methods of ExponentialHistogramOptions using a sample bucketing scheme. | ||
func TestExponentialHistogramOptions(t *testing.T) { | ||
o, err := NewExponentialHistogramOptions(100.0, 10.0, 2.0, epsilon) | ||
assert.Nil(t, err) | ||
assert.Equal(t, epsilon, o.Epsilon()) | ||
assert.Equal(t, 6, o.NumBuckets()) | ||
|
||
assert.Equal(t, 0.0, o.GetBucketStart(0)) | ||
assert.Equal(t, 10.0, o.GetBucketStart(1)) | ||
assert.Equal(t, 20.0, o.GetBucketStart(2)) | ||
assert.Equal(t, 40.0, o.GetBucketStart(3)) | ||
assert.Equal(t, 80.0, o.GetBucketStart(4)) | ||
assert.Equal(t, 160.0, o.GetBucketStart(5)) | ||
|
||
assert.Equal(t, 0, o.FindBucket(-1.0)) | ||
assert.Equal(t, 0, o.FindBucket(9.99)) | ||
assert.Equal(t, 1, o.FindBucket(10.0)) | ||
assert.Equal(t, 2, o.FindBucket(20.0)) | ||
assert.Equal(t, 5, o.FindBucket(200.0)) | ||
} |
Oops, something went wrong.