Skip to content

Commit aac0ec6

Browse files
committed
Add unit tests for kinematics modules
1 parent a531871 commit aac0ec6

7 files changed

Lines changed: 1117 additions & 0 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { PIDController } from '@/core/kinematics/pid'
4+
5+
describe('PIDController', () => {
6+
describe('P-only controller', () => {
7+
it('returns correction proportional to error', () => {
8+
const pid = new PIDController(1, 0, 0)
9+
const error = [1, 0, 0]
10+
11+
const result = pid.compute(error)
12+
13+
// P = Kp * error = 1 * [1,0,0] = [1,0,0]
14+
expect(result.correctionVector).toEqual([1, 0, 0])
15+
expect(result.magnitude).toBeCloseTo(1)
16+
})
17+
18+
it('scales correction by Kp', () => {
19+
const pid = new PIDController(0.5, 0, 0)
20+
const error = [2, 0, 0]
21+
22+
const result = pid.compute(error)
23+
24+
// P = 0.5 * [2,0,0] = [1,0,0]
25+
expect(result.correctionVector).toEqual([1, 0, 0])
26+
})
27+
28+
it('handles multi-dimensional error', () => {
29+
const pid = new PIDController(1, 0, 0)
30+
const error = [1, 2, 3]
31+
32+
const result = pid.compute(error)
33+
34+
expect(result.correctionVector).toEqual([1, 2, 3])
35+
})
36+
})
37+
38+
describe('I-only controller', () => {
39+
it('accumulates error over time', () => {
40+
const pid = new PIDController(0, 1, 0)
41+
42+
pid.compute([1, 0, 0]) // integral = [1,0,0]
43+
const result = pid.compute([1, 0, 0]) // integral = [2,0,0]
44+
45+
// I = Ki * integral = 1 * [2,0,0] = [2,0,0]
46+
expect(result.correctionVector).toEqual([2, 0, 0])
47+
})
48+
49+
it('scales by Ki', () => {
50+
const pid = new PIDController(0, 0.5, 0)
51+
52+
pid.compute([2, 0, 0]) // integral = [2,0,0]
53+
54+
// I = 0.5 * [2,0,0] = [1,0,0]
55+
const result = pid.compute([0, 0, 0]) // integral = [2,0,0] (no new error)
56+
57+
expect(result.correctionVector).toEqual([1, 0, 0])
58+
})
59+
})
60+
61+
describe('D-only controller', () => {
62+
it('returns zero on first call (no previous error)', () => {
63+
const pid = new PIDController(0, 0, 1)
64+
const error = [1, 0, 0]
65+
66+
const result = pid.compute(error)
67+
68+
// D = Kd * (error - lastError) / dt
69+
// On first call, lastError defaults to error, so derivative = 0
70+
expect(result.correctionVector).toEqual([0, 0, 0])
71+
expect(result.magnitude).toBe(0)
72+
})
73+
74+
it('responds to change in error', () => {
75+
const pid = new PIDController(0, 0, 1)
76+
77+
pid.compute([0, 0, 0]) // Set lastError = [0,0,0]
78+
const result = pid.compute([1, 0, 0]) // derivative = [1,0,0]
79+
80+
// D = 1 * [1,0,0] = [1,0,0]
81+
expect(result.correctionVector).toEqual([1, 0, 0])
82+
})
83+
})
84+
85+
describe('Full PID controller', () => {
86+
it('combines P, I, D terms', () => {
87+
const pid = new PIDController(1, 0.1, 0.5)
88+
89+
// First call: error = [1,0,0]
90+
// P = [1,0,0], I = 0.1*[1,0,0] = [0.1,0,0], D = 0 (first call)
91+
const result1 = pid.compute([1, 0, 0])
92+
expect(result1.correctionVector[0]).toBeCloseTo(1.1) // P + I + D = 1 + 0.1 + 0
93+
94+
// Second call: error = [2,0,0]
95+
// P = [2,0,0], I = 0.1*[3,0,0] = [0.3,0,0], D = 0.5*[1,0,0] = [0.5,0,0]
96+
const result2 = pid.compute([2, 0, 0])
97+
expect(result2.correctionVector[0]).toBeCloseTo(2.8) // 2 + 0.3 + 0.5
98+
})
99+
})
100+
101+
describe('Stability detection', () => {
102+
it('reports stable when correction magnitude < threshold', () => {
103+
const pid = new PIDController(1, 0, 0, 0.5)
104+
const error = [0.1, 0, 0]
105+
106+
const result = pid.compute(error)
107+
108+
expect(result.isStable).toBe(true)
109+
expect(result.magnitude).toBeCloseTo(0.1)
110+
})
111+
112+
it('reports unstable when correction magnitude >= threshold', () => {
113+
const pid = new PIDController(1, 0, 0, 0.1)
114+
const error = [1, 0, 0]
115+
116+
const result = pid.compute(error)
117+
118+
expect(result.isStable).toBe(false)
119+
expect(result.magnitude).toBeCloseTo(1)
120+
})
121+
122+
it('uses default threshold of 0.1', () => {
123+
const pid = new PIDController(1, 0, 0)
124+
125+
const stableResult = pid.compute([0.05, 0, 0])
126+
expect(stableResult.isStable).toBe(true)
127+
128+
pid.reset()
129+
const unstableResult = pid.compute([0.5, 0, 0])
130+
expect(unstableResult.isStable).toBe(false)
131+
})
132+
})
133+
134+
describe('Log output', () => {
135+
it('includes P, I, D norms in log string', () => {
136+
const pid = new PIDController(1, 0.1, 0.5)
137+
const result = pid.compute([1, 0, 0])
138+
139+
expect(result.log).toMatch(/^PID\(P=/)
140+
expect(result.log).toContain('I=')
141+
expect(result.log).toContain('D=')
142+
})
143+
})
144+
145+
describe('Reset', () => {
146+
it('clears integral and lastError', () => {
147+
const pid = new PIDController(0, 1, 0)
148+
149+
// Accumulate integral
150+
pid.compute([1, 0, 0])
151+
pid.compute([1, 0, 0])
152+
153+
pid.reset()
154+
155+
// After reset, integral should be zero again
156+
const result = pid.compute([1, 0, 0])
157+
// I = Ki * integral = 1 * [1,0,0] = [1,0,0] (fresh start)
158+
expect(result.correctionVector).toEqual([1, 0, 0])
159+
})
160+
})
161+
162+
describe('dt parameter', () => {
163+
it('scales integral by dt', () => {
164+
const pid = new PIDController(0, 1, 0)
165+
166+
const result = pid.compute([1, 0, 0], 2)
167+
168+
// integral = error * dt = [2,0,0]
169+
// I = Ki * integral = 1 * [2,0,0] = [2,0,0]
170+
expect(result.correctionVector).toEqual([2, 0, 0])
171+
})
172+
173+
it('scales derivative by 1/dt', () => {
174+
const pid = new PIDController(0, 0, 1)
175+
176+
pid.compute([0, 0, 0], 2)
177+
const result = pid.compute([2, 0, 0], 2)
178+
179+
// derivative = (error - lastError) / dt = [2,0,0] / 2 = [1,0,0]
180+
// D = Kd * derivative = 1 * [1,0,0] = [1,0,0]
181+
expect(result.correctionVector).toEqual([1, 0, 0])
182+
})
183+
})
184+
185+
describe('Edge cases', () => {
186+
it('handles zero error', () => {
187+
const pid = new PIDController(1, 0.1, 0.5)
188+
const result = pid.compute([0, 0, 0])
189+
190+
expect(result.correctionVector).toEqual([0, 0, 0])
191+
expect(result.magnitude).toBe(0)
192+
expect(result.isStable).toBe(true)
193+
})
194+
195+
it('handles high-dimensional vectors', () => {
196+
const pid = new PIDController(1, 0, 0)
197+
const error = Array.from({ length: 1536 }, (_, i) => i * 0.001)
198+
199+
const result = pid.compute(error)
200+
201+
expect(result.correctionVector).toHaveLength(1536)
202+
expect(result.magnitude).toBeGreaterThan(0)
203+
})
204+
})
205+
})

0 commit comments

Comments
 (0)