github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/graphite/ts/lttb.go (about)

     1  // Copyright (c) 2019 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package ts
    22  
    23  import (
    24  	"math"
    25  	"time"
    26  )
    27  
    28  // LTTB down-samples the data to contain only threshold number of points that
    29  // have the same visual shape as the original data. Inspired from
    30  // https://github.com/dgryski/go-lttb which is based on
    31  // https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf
    32  func LTTB(b *Series, start time.Time, end time.Time, millisPerStep int) *Series {
    33  	if end.After(b.EndTime()) {
    34  		end = b.EndTime()
    35  	}
    36  
    37  	seriesValuesPerStep := millisPerStep / b.MillisPerStep()
    38  	seriesStart, seriesEnd := b.StepAtTime(start), b.StepAtTime(end)
    39  
    40  	// This threshold is different than max datapoints since we ensure step size is an integer multiple of original series step
    41  	threshold := int(math.Ceil(float64(seriesEnd-seriesStart) / float64(seriesValuesPerStep)))
    42  	if threshold == 0 || threshold > b.Len() {
    43  		return b // Nothing to do
    44  	}
    45  
    46  	values := NewValues(b.ctx, millisPerStep, threshold)
    47  	// Bucket size. Leave room for start and end data points
    48  	every := float64(seriesValuesPerStep)
    49  	// Always add the first point
    50  	values.SetValueAt(0, b.ValueAt(seriesStart))
    51  	// Set a to be the first chosen point
    52  	a := seriesStart
    53  
    54  	bucketStart := seriesStart + 1
    55  	bucketCenter := bucketStart + int(math.Floor(every)) + 1
    56  
    57  	for i := 0; i < threshold-2; i++ {
    58  		bucketEnd := bucketCenter + int(math.Floor(every))
    59  
    60  		// Calculate point average for next bucket (containing c)
    61  		avgRangeStart := bucketCenter
    62  		avgRangeEnd := bucketEnd
    63  
    64  		if avgRangeEnd >= seriesEnd {
    65  			avgRangeEnd = seriesEnd
    66  		}
    67  
    68  		avgRangeLength := float64(avgRangeEnd - avgRangeStart)
    69  
    70  		var avgX, avgY float64
    71  		var valuesRead int
    72  		for ; avgRangeStart < avgRangeEnd; avgRangeStart++ {
    73  			yVal := b.ValueAt(avgRangeStart)
    74  			if math.IsNaN(yVal) {
    75  				continue
    76  			}
    77  			valuesRead++
    78  			avgX += float64(avgRangeStart)
    79  			avgY += yVal
    80  		}
    81  
    82  		if valuesRead > 0 {
    83  			avgX /= avgRangeLength
    84  			avgY /= avgRangeLength
    85  		} else {
    86  			// If all nulls then should not assign a value to average
    87  			avgX = math.NaN()
    88  			avgY = math.NaN()
    89  		}
    90  
    91  		// Get the range for this bucket
    92  		rangeOffs := bucketStart
    93  		rangeTo := bucketCenter
    94  
    95  		// Point a
    96  		pointAX := float64(a)
    97  		pointAY := b.ValueAt(a)
    98  
    99  		var nextA int
   100  
   101  		// If all points in left or right bucket are null, then fallback to average
   102  		if math.IsNaN(avgY) || math.IsNaN(pointAY) {
   103  			nextA = indexClosestToAverage(b, rangeOffs, rangeTo)
   104  		} else {
   105  			nextA = indexWithLargestTriangle(b, rangeOffs, rangeTo, pointAX, pointAY, avgX, avgY)
   106  		}
   107  
   108  		values.SetValueAt(i+1, b.ValueAt(nextA)) // Pick this point from the bucket
   109  		a = nextA                                // This a is the next a (chosen b)
   110  
   111  		bucketStart = bucketCenter
   112  		bucketCenter = bucketEnd
   113  	}
   114  
   115  	if values.Len() > 1 {
   116  		// Always add last if not just a single step
   117  		values.SetValueAt(values.Len()-1, b.ValueAt(seriesEnd-1))
   118  	}
   119  
   120  	// Derive a new series
   121  	sampledSeries := b.DerivedSeries(start, values)
   122  	return sampledSeries
   123  }
   124  
   125  func indexWithLargestTriangle(b *Series, start int, end int, leftX float64, leftY float64, rightX float64, rightY float64) int {
   126  	// The original algorithm implementation initializes the maxArea as 0 which is a bug!
   127  	maxArea := -1.0
   128  	var largestIndex int
   129  
   130  	xDifference := leftX - rightX
   131  	yDifference := rightY - leftY
   132  	for index := start; index < end; index++ {
   133  		// Calculate triangle area over three buckets
   134  		area := xDifference*(b.ValueAt(index)-leftY) - (leftX-float64(index))*yDifference
   135  		// We only care about the relative area here.
   136  		area = math.Abs(area)
   137  		// Handle nulls properly
   138  		if math.IsNaN(area) {
   139  			area = 0
   140  		}
   141  
   142  		if area > maxArea {
   143  			maxArea = area
   144  			largestIndex = index
   145  		}
   146  	}
   147  
   148  	return largestIndex
   149  }
   150  
   151  func indexClosestToAverage(b *Series, start int, end int) int {
   152  	var sum float64
   153  	var count int
   154  	for index := start; index < end; index++ {
   155  		if math.IsNaN(b.ValueAt(index)) {
   156  			continue
   157  		}
   158  
   159  		sum += b.ValueAt(index)
   160  		count++
   161  	}
   162  
   163  	if count == 0 {
   164  		return start
   165  	}
   166  
   167  	average := sum / float64(count)
   168  	minDifference := math.MaxFloat64
   169  	closestIndex := start
   170  	for index := start; index < end; index++ {
   171  		difference := math.Abs(average - b.ValueAt(index))
   172  		if !math.IsNaN(b.ValueAt(index)) && difference < minDifference {
   173  			closestIndex = index
   174  			minDifference = difference
   175  		}
   176  	}
   177  
   178  	return closestIndex
   179  }