pid_test.go raw

   1  package ratelimit
   2  
   3  import (
   4  	"testing"
   5  	"time"
   6  )
   7  
   8  func TestPIDController_BasicOperation(t *testing.T) {
   9  	pid := DefaultPIDControllerForWrites()
  10  
  11  	// First call should return 0 (initialization)
  12  	delay := pid.Update(0.5)
  13  	if delay != 0 {
  14  		t.Errorf("expected 0 delay on first call, got %v", delay)
  15  	}
  16  
  17  	// Sleep a bit to ensure dt > 0
  18  	time.Sleep(10 * time.Millisecond)
  19  
  20  	// Process variable below setpoint (0.5 < 0.85) should return 0 delay
  21  	delay = pid.Update(0.5)
  22  	if delay != 0 {
  23  		t.Errorf("expected 0 delay when below setpoint, got %v", delay)
  24  	}
  25  
  26  	// Process variable above setpoint should return positive delay
  27  	time.Sleep(10 * time.Millisecond)
  28  	delay = pid.Update(0.95) // 0.95 > 0.85 setpoint
  29  	if delay <= 0 {
  30  		t.Errorf("expected positive delay when above setpoint, got %v", delay)
  31  	}
  32  }
  33  
  34  func TestPIDController_IntegralAccumulation(t *testing.T) {
  35  	pid := NewPIDController(
  36  		0.5, 0.5, 0.0, // High Ki, no Kd
  37  		0.5,  // setpoint
  38  		0.2,  // filter alpha
  39  		-10, 10, // integral bounds
  40  		0, 1.0, // output bounds
  41  	)
  42  
  43  	// Initialize
  44  	pid.Update(0.5)
  45  	time.Sleep(10 * time.Millisecond)
  46  
  47  	// Continuously above setpoint should accumulate integral
  48  	for i := 0; i < 10; i++ {
  49  		time.Sleep(10 * time.Millisecond)
  50  		pid.Update(0.8) // 0.3 above setpoint
  51  	}
  52  
  53  	integral, _, _ := pid.GetState()
  54  	if integral <= 0 {
  55  		t.Errorf("expected positive integral after sustained error, got %v", integral)
  56  	}
  57  }
  58  
  59  func TestPIDController_FilteredDerivative(t *testing.T) {
  60  	pid := NewPIDController(
  61  		0.0, 0.0, 1.0, // Only Kd
  62  		0.5,  // setpoint
  63  		0.5,  // 50% filtering
  64  		-10, 10,
  65  		0, 1.0,
  66  	)
  67  
  68  	// Initialize with low value
  69  	pid.Update(0.5)
  70  	time.Sleep(10 * time.Millisecond)
  71  
  72  	// Second call with same value - derivative should be near zero
  73  	pid.Update(0.5)
  74  	_, _, prevFiltered := pid.GetState()
  75  
  76  	time.Sleep(10 * time.Millisecond)
  77  
  78  	// Big jump - filtered derivative should be dampened
  79  	delay := pid.Update(1.0)
  80  
  81  	// The filtered derivative should cause some response, but dampened
  82  	// Since we only have Kd=1.0 and alpha=0.5, the response should be modest
  83  	if delay < 0 {
  84  		t.Errorf("expected non-negative delay, got %v", delay)
  85  	}
  86  
  87  	_, _, newFiltered := pid.GetState()
  88  	// Filtered error should have moved toward the new error but not fully
  89  	if newFiltered <= prevFiltered {
  90  		t.Errorf("filtered error should increase with rising process variable")
  91  	}
  92  }
  93  
  94  func TestPIDController_AntiWindup(t *testing.T) {
  95  	pid := NewPIDController(
  96  		0.0, 1.0, 0.0, // Only Ki
  97  		0.5,     // setpoint
  98  		0.2,     // filter alpha
  99  		-1.0, 1.0, // tight integral bounds
 100  		0, 10.0, // wide output bounds
 101  	)
 102  
 103  	// Initialize
 104  	pid.Update(0.5)
 105  
 106  	// Drive the integral to its limit
 107  	for i := 0; i < 100; i++ {
 108  		time.Sleep(1 * time.Millisecond)
 109  		pid.Update(1.0) // Large positive error
 110  	}
 111  
 112  	integral, _, _ := pid.GetState()
 113  	if integral > 1.0 {
 114  		t.Errorf("integral should be clamped at 1.0, got %v", integral)
 115  	}
 116  }
 117  
 118  func TestPIDController_Reset(t *testing.T) {
 119  	pid := DefaultPIDControllerForWrites()
 120  
 121  	// Build up some state
 122  	pid.Update(0.5)
 123  	time.Sleep(10 * time.Millisecond)
 124  	pid.Update(0.9)
 125  	time.Sleep(10 * time.Millisecond)
 126  	pid.Update(0.95)
 127  
 128  	// Reset
 129  	pid.Reset()
 130  
 131  	integral, prevErr, prevFiltered := pid.GetState()
 132  	if integral != 0 || prevErr != 0 || prevFiltered != 0 {
 133  		t.Errorf("expected all state to be zero after reset")
 134  	}
 135  
 136  	// Next call should behave like first call
 137  	delay := pid.Update(0.9)
 138  	if delay != 0 {
 139  		t.Errorf("expected 0 delay on first call after reset, got %v", delay)
 140  	}
 141  }
 142  
 143  func TestPIDController_SetGains(t *testing.T) {
 144  	pid := DefaultPIDControllerForWrites()
 145  
 146  	// Change gains
 147  	pid.SetGains(1.0, 0.5, 0.1)
 148  
 149  	if pid.Kp != 1.0 || pid.Ki != 0.5 || pid.Kd != 0.1 {
 150  		t.Errorf("gains not updated correctly")
 151  	}
 152  }
 153  
 154  func TestPIDController_SetSetpoint(t *testing.T) {
 155  	pid := DefaultPIDControllerForWrites()
 156  
 157  	pid.SetSetpoint(0.7)
 158  
 159  	if pid.Setpoint != 0.7 {
 160  		t.Errorf("setpoint not updated, got %v", pid.Setpoint)
 161  	}
 162  }
 163  
 164  func TestDefaultControllers(t *testing.T) {
 165  	writePID := DefaultPIDControllerForWrites()
 166  	readPID := DefaultPIDControllerForReads()
 167  
 168  	// Write controller should have higher gains and lower setpoint
 169  	if writePID.Kp <= readPID.Kp {
 170  		t.Errorf("write Kp should be higher than read Kp")
 171  	}
 172  
 173  	if writePID.Setpoint >= readPID.Setpoint {
 174  		t.Errorf("write setpoint should be lower than read setpoint")
 175  	}
 176  }
 177