Skip to content

Commit d841e8e

Browse files
Use simulated time in tests
This has a few advantages: - Since time is frozen unless explicit resets or ticks happen, how long the test code takes to execute does not affect the test results. Specifically, this means that the `sample_time` can be matched precisely, and that makes for tests that are easier to reason about. - Since time is controlled, it can flow at any speed we want. Two consecutive calls to `stime.tick()` will make two seconds pass as fast as your machine can execute the two statements. The first advantage makes this test suite deterministic (because the PID configuration is stable in convergence tests). The second speeds up the overall execution of the test suite by a factor of a hundred on my machine (from ~26s to ~200ms).
1 parent d4b030c commit d841e8e

File tree

2 files changed

+51
-39
lines changed

2 files changed

+51
-39
lines changed

pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ classifiers = [
2020
"Documentation" = "https://simple-pid.readthedocs.io/"
2121

2222
[project.optional-dependencies]
23-
test = ["pytest"]
23+
test = [
24+
"pytest",
25+
"stime",
26+
]
2427
doc = [
2528
"furo==2023.3.27",
2629
"myst-parser==1.0.0",

tests/test_pid.py

+47-38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import sys
22
import time
3+
import stime
34
import pytest
45
from simple_pid import PID
56

@@ -25,59 +26,65 @@ def test_P_negative_setpoint():
2526

2627

2728
def test_I():
28-
pid = PID(0, 10, 0, setpoint=10, sample_time=0.1)
29-
time.sleep(0.1)
29+
stime.reset(0)
30+
pid = PID(0, 10, 0, setpoint=10, sample_time=0.1, time_fn=stime.monotonic)
31+
stime.reset(0.1)
3032

3133
assert round(pid(0)) == 10.0 # Make sure we are close to expected value
32-
time.sleep(0.1)
34+
stime.reset(0.2)
3335

3436
assert round(pid(0)) == 20.0
3537

3638

3739
def test_I_negative_setpoint():
38-
pid = PID(0, 10, 0, setpoint=-10, sample_time=0.1)
39-
time.sleep(0.1)
40+
stime.reset(0)
41+
pid = PID(0, 10, 0, setpoint=-10, sample_time=0.1, time_fn=stime.monotonic)
42+
stime.reset(0.1)
43+
4044

4145
assert round(pid(0)) == -10.0
42-
time.sleep(0.1)
46+
stime.reset(0.2)
4347

4448
assert round(pid(0)) == -20.0
4549

4650

4751
def test_D():
48-
pid = PID(0, 0, 0.1, setpoint=10, sample_time=0.1)
52+
stime.reset(0)
53+
pid = PID(0, 0, 0.1, setpoint=10, sample_time=0.1, time_fn=stime.monotonic)
4954

5055
# Should not compute derivative when there is no previous input (don't assume 0 as first input)
5156
assert pid(0) == 0
52-
time.sleep(0.1)
57+
stime.reset(0.1)
58+
5359

5460
# Derivative is 0 when input is the same
5561
assert pid(0) == 0
5662
assert pid(0) == 0
57-
time.sleep(0.1)
63+
stime.reset(0.2)
5864

5965
assert round(pid(5)) == -5
60-
time.sleep(0.1)
66+
stime.reset(0.3)
6167
assert round(pid(15)) == -10
6268

6369

6470
def test_D_negative_setpoint():
65-
pid = PID(0, 0, 0.1, setpoint=-10, sample_time=0.1)
66-
time.sleep(0.1)
71+
stime.reset(0)
72+
pid = PID(0, 0, 0.1, setpoint=-10, sample_time=0.1, time_fn=stime.monotonic)
73+
stime.reset(0.1)
6774

6875
# Should not compute derivative when there is no previous input (don't assume 0 as first input)
6976
assert pid(0) == 0
70-
time.sleep(0.1)
77+
stime.reset(0.2)
7178

7279
# Derivative is 0 when input is the same
7380
assert pid(0) == 0
7481
assert pid(0) == 0
75-
time.sleep(0.1)
82+
stime.reset(0.3)
7683

7784
assert round(pid(5)) == -5
78-
time.sleep(0.1)
85+
stime.reset(0.4)
7986
assert round(pid(-5)) == 10
80-
time.sleep(0.1)
87+
stime.reset(0.5)
8188
assert round(pid(-15)) == 10
8289

8390

@@ -89,11 +96,12 @@ def test_desired_state():
8996

9097

9198
def test_output_limits():
92-
pid = PID(100, 20, 40, setpoint=10, output_limits=(0, 100), sample_time=None)
93-
time.sleep(0.1)
99+
stime.reset(0)
100+
pid = PID(100, 20, 40, setpoint=10, output_limits=(0, 100), sample_time=None, time_fn=stime.monotonic)
101+
stime.reset(0.1)
94102

95103
assert 0 <= pid(0) <= 100
96-
time.sleep(0.1)
104+
stime.reset(0.2)
97105

98106
assert 0 <= pid(-100) <= 100
99107

@@ -156,7 +164,8 @@ def test_starting_output():
156164

157165

158166
def test_auto_mode():
159-
pid = PID(1, 0, 0, setpoint=10, sample_time=None)
167+
stime.reset(0)
168+
pid = PID(1, 0, 0, setpoint=10, sample_time=None, time_fn=stime.monotonic)
160169

161170
# Ensure updates happen by default
162171
assert pid(0) == 10
@@ -175,7 +184,7 @@ def test_auto_mode():
175184

176185
# Last update time should be reset to avoid huge dt
177186
pid.auto_mode = False
178-
time.sleep(1)
187+
stime.reset(1)
179188
pid.auto_mode = True
180189
assert pid.time_fn() - pid._last_time < 0.01
181190

@@ -186,11 +195,12 @@ def test_auto_mode():
186195

187196

188197
def test_separate_components():
189-
pid = PID(1, 0, 1, setpoint=10, sample_time=0.1)
198+
stime.reset(0)
199+
pid = PID(1, 0, 1, setpoint=10, sample_time=0.1, time_fn=stime.monotonic)
190200

191201
assert pid(0) == 10
192202
assert pid.components == (10, 0, 0)
193-
time.sleep(0.1)
203+
stime.reset(0.1)
194204

195205
assert round(pid(5)) == -45
196206
assert tuple(round(term) for term in pid.components) == (5, 0, -50)
@@ -236,46 +246,45 @@ def test_repr():
236246

237247

238248
def test_converge_system():
239-
TIME_TO_CONVERGE = 12
240-
241-
pid = PID(1, 0.8, 0.04, setpoint=5, output_limits=(-5, 5))
249+
stime.reset(0)
250+
pid = PID(1, 0.8, 0.04, setpoint=5, output_limits=(-5, 5), time_fn=stime.monotonic)
242251
pv = 0 # Process variable
243252

244253
def update_system(c, dt):
245254
# Calculate a simple system model
246255
return pv + c * dt - 1 * dt
247256

248-
start_time = time.time()
257+
start_time = stime.time()
249258
last_time = start_time
250259

251-
while time.time() - start_time < TIME_TO_CONVERGE:
260+
for _ in range(5):
261+
stime.tick()
252262
c = pid(pv)
253-
pv = update_system(c, time.time() - last_time)
263+
pv = update_system(c, stime.time() - last_time)
254264

255-
last_time = time.time()
265+
last_time = stime.time()
256266

257267
# Check if system has converged
258268
assert abs(pv - 5) < 0.1
259269

260-
261270
def test_converge_diff_on_error():
262-
TIME_TO_CONVERGE = 12
263-
264-
pid = PID(1, 0.8, 0.04, setpoint=5, output_limits=(-5, 5), differential_on_measurement=False)
271+
stime.reset(0)
272+
pid = PID(1, 0.8, 0.04, setpoint=5, output_limits=(-5, 5), differential_on_measurement=False, time_fn=stime.monotonic)
265273
pv = 0 # Process variable
266274

267275
def update_system(c, dt):
268276
# Calculate a simple system model
269277
return pv + c * dt - 1 * dt
270278

271-
start_time = time.time()
279+
start_time = stime.time()
272280
last_time = start_time
273281

274-
while time.time() - start_time < TIME_TO_CONVERGE:
282+
for _ in range(5):
283+
stime.tick()
275284
c = pid(pv)
276-
pv = update_system(c, time.time() - last_time)
285+
pv = update_system(c, stime.time() - last_time)
277286

278-
last_time = time.time()
287+
last_time = stime.time()
279288

280289
# Check if system has converged
281290
assert abs(pv - 5) < 0.1

0 commit comments

Comments
 (0)