github.com/MetalBlockchain/metalgo@v1.11.9/utils/math/continuous_averager.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package math
     5  
     6  import (
     7  	"math"
     8  	"time"
     9  )
    10  
    11  var convertEToBase2 = math.Log(2)
    12  
    13  type continuousAverager struct {
    14  	halflife    float64
    15  	weightedSum float64
    16  	normalizer  float64
    17  	lastUpdated time.Time
    18  }
    19  
    20  // NewUninitializedAverager creates a new averager with the given halflife. If
    21  // [Read] is called before [Observe], the zero value will be returned. When
    22  // [Observe] is called the first time, the averager will be initialized with
    23  // [value] at that time.
    24  func NewUninitializedAverager(halfLife time.Duration) Averager {
    25  	// Use 0 as the initialPrediction and 0 as the currentTime, so that when the
    26  	// first observation occurs (at a non-zero time) the initial prediction's
    27  	// weight will become negligible.
    28  	return NewAverager(0, halfLife, time.Time{})
    29  }
    30  
    31  func NewAverager(
    32  	initialPrediction float64,
    33  	halflife time.Duration,
    34  	currentTime time.Time,
    35  ) Averager {
    36  	return &continuousAverager{
    37  		halflife:    float64(halflife) / convertEToBase2,
    38  		weightedSum: initialPrediction,
    39  		normalizer:  1,
    40  		lastUpdated: currentTime,
    41  	}
    42  }
    43  
    44  func (a *continuousAverager) Observe(value float64, currentTime time.Time) {
    45  	delta := a.lastUpdated.Sub(currentTime)
    46  	switch {
    47  	case delta < 0:
    48  		// If the times are called in order, scale the previous values to keep the
    49  		// sizes manageable
    50  		newWeight := math.Exp(float64(delta) / a.halflife)
    51  
    52  		a.weightedSum = value + newWeight*a.weightedSum
    53  		a.normalizer = 1 + newWeight*a.normalizer
    54  
    55  		a.lastUpdated = currentTime
    56  	case delta == 0:
    57  		// If this is called multiple times at the same wall clock time, no
    58  		// scaling needs to occur
    59  		a.weightedSum += value
    60  		a.normalizer++
    61  	default:
    62  		// If the times are called out of order, don't scale the previous values
    63  		newWeight := math.Exp(float64(-delta) / a.halflife)
    64  
    65  		a.weightedSum += newWeight * value
    66  		a.normalizer += newWeight
    67  	}
    68  }
    69  
    70  func (a *continuousAverager) Read() float64 {
    71  	return a.weightedSum / a.normalizer
    72  }