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