github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/cruisectl/aggregators.go (about)

     1  package cruisectl
     2  
     3  import (
     4  	"fmt"
     5  )
     6  
     7  // Ewma implements the exponentially weighted moving average with smoothing factor α.
     8  // The Ewma is a filter commonly applied to time-discrete signals. Mathematically,
     9  // it is represented by the recursive update formula
    10  //
    11  //	value ← α·v + (1-α)·value
    12  //
    13  // where `v` the next observation. Intuitively, the loss factor `α` relates to the
    14  // time window of N observations that we average over. For example, let
    15  // α ≡ 1/N and consider an input that suddenly changes from x to y as a step
    16  // function. Then N is _roughly_ the number of samples required to move the output
    17  // average about 2/3 of the way from x to y.
    18  // For numeric stability, we require α to satisfy 0 < a < 1.
    19  // Not concurrency safe.
    20  type Ewma struct {
    21  	alpha float64
    22  	value float64
    23  }
    24  
    25  // NewEwma instantiates a new exponentially weighted moving average.
    26  // The smoothing factor `alpha` relates to the averaging time window. Let `alpha` ≡ 1/N and
    27  // consider an input that suddenly changes from x to y as a step function. Then N is roughly
    28  // the number of samples required to move the output average about 2/3 of the way from x to y.
    29  // For numeric stability, we require `alpha` to satisfy 0 < `alpha` < 1.
    30  func NewEwma(alpha, initialValue float64) (Ewma, error) {
    31  	if (alpha <= 0) || (1 <= alpha) {
    32  		return Ewma{}, fmt.Errorf("for numeric stability, we require the smoothing factor to satisfy 0 < alpha < 1")
    33  	}
    34  	return Ewma{
    35  		alpha: alpha,
    36  		value: initialValue,
    37  	}, nil
    38  }
    39  
    40  // AddRepeatedObservation adds k consecutive observations with the same value v. Returns the updated value.
    41  func (e *Ewma) AddRepeatedObservation(v float64, k int) float64 {
    42  	// closed from for k consecutive updates with the same observation v:
    43  	//   value ← r·value + v·(1-r)   with r := (1-α)^k
    44  	r := powWithIntegerExponent(1.0-e.alpha, k)
    45  	e.value = r*e.value + v*(1.0-r)
    46  	return e.value
    47  }
    48  
    49  // AddObservation adds the value `v` to the EWMA. Returns the updated value.
    50  func (e *Ewma) AddObservation(v float64) float64 {
    51  	// Update formula: value ← α·v + (1-α)·value = value + α·(v - value)
    52  	e.value = e.value + e.alpha*(v-e.value)
    53  	return e.value
    54  }
    55  
    56  func (e *Ewma) Value() float64 {
    57  	return e.value
    58  }
    59  
    60  // LeakyIntegrator is a filter commonly applied to time-discrete signals.
    61  // Intuitively, it sums values over a limited time window. This implementation is
    62  // parameterized by the loss factor `ß`:
    63  //
    64  //	value ← v + (1-ß)·value
    65  //
    66  // where `v` the next observation. Intuitively, the loss factor `ß` relates to the
    67  // time window of N observations that we integrate over. For example, let ß ≡ 1/N
    68  // and consider a constant input x:
    69  //   - the integrator value will saturate at x·N
    70  //   - an integrator initialized at 0 reaches 2/3 of the saturation value after N samples
    71  //
    72  // For numeric stability, we require ß to satisfy 0 < ß < 1.
    73  // Further details on Leaky Integrator: https://www.music.mcgill.ca/~gary/307/week2/node4.html
    74  // Not concurrency safe.
    75  type LeakyIntegrator struct {
    76  	feedbackCoef float64 // feedback coefficient := (1-ß)
    77  	value        float64
    78  }
    79  
    80  // NewLeakyIntegrator instantiates a new leaky integrator with loss factor `beta`, where
    81  // `beta relates to window of N observations that we integrate over. For example, let
    82  // `beta` ≡ 1/N and consider a constant input x. The integrator value will saturate at x·N.
    83  // An integrator initialized at 0 reaches 2/3 of the saturation value after N samples.
    84  // For numeric stability, we require `beta` to satisfy 0 < `beta` < 1.
    85  func NewLeakyIntegrator(beta, initialValue float64) (LeakyIntegrator, error) {
    86  	if (beta <= 0) || (1 <= beta) {
    87  		return LeakyIntegrator{}, fmt.Errorf("for numeric stability, we require the loss factor to satisfy 0 < beta < 1")
    88  	}
    89  	return LeakyIntegrator{
    90  		feedbackCoef: 1.0 - beta,
    91  		value:        initialValue,
    92  	}, nil
    93  }
    94  
    95  // AddRepeatedObservation adds k consecutive observations with the same value v.  Returns the updated value.
    96  func (e *LeakyIntegrator) AddRepeatedObservation(v float64, k int) float64 {
    97  	// closed from for k consecutive updates with the same observation v:
    98  	//   value ← r·value + v·(1-r)   with r := α^k
    99  	r := powWithIntegerExponent(e.feedbackCoef, k)
   100  	e.value = r*e.value + v*(1.0-r)/(1.0-e.feedbackCoef)
   101  	return e.value
   102  }
   103  
   104  // AddObservation adds the value `v` to the LeakyIntegrator. Returns the updated value.
   105  func (e *LeakyIntegrator) AddObservation(v float64) float64 {
   106  	// Update formula: value ← v + feedbackCoef·value
   107  	// where feedbackCoef = (1-beta)
   108  	e.value = v + e.feedbackCoef*e.value
   109  	return e.value
   110  }
   111  
   112  func (e *LeakyIntegrator) Value() float64 {
   113  	return e.value
   114  }
   115  
   116  // powWithIntegerExponent implements exponentiation b^k optimized for integer k >=1
   117  func powWithIntegerExponent(b float64, k int) float64 {
   118  	r := 1.0
   119  	for {
   120  		if k&1 == 1 {
   121  			r *= b
   122  		}
   123  		k >>= 1
   124  		if k == 0 {
   125  			break
   126  		}
   127  		b *= b
   128  	}
   129  	return r
   130  }