Skip to content

Commit e14d132

Browse files
authored
Merge pull request #134 from florisvb/fix-pi-cruise-control
Updated and fixed cruise control example per #133
2 parents a385711 + b4186aa commit e14d132

6 files changed

Lines changed: 287 additions & 321 deletions

examples/1_basic_tutorial.ipynb

Lines changed: 86 additions & 78 deletions
Large diffs are not rendered by default.

examples/2a_optimizing_parameters_with_dxdt_known.ipynb

Lines changed: 88 additions & 80 deletions
Large diffs are not rendered by default.

examples/2b_optimizing_parameters_with_dxdt_unknown.ipynb

Lines changed: 55 additions & 55 deletions
Large diffs are not rendered by default.

examples/3_automatic_method_suggestion.ipynb

Lines changed: 11 additions & 18 deletions
Large diffs are not rendered by default.

pynumdiff/tests/test_optimize.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from ..linear_model import spectraldiff, polydiff, savgoldiff
77
from ..total_variation_regularization import velocity, acceleration, iterative_velocity
88
from ..optimize import optimize
9-
from ..utils.simulate import pi_control
9+
from ..utils.simulate import pi_cruise_control
1010

1111

1212
# simulation
1313
dt = 0.01
14-
x, x_truth, dxdt_truth, extras = pi_control(duration=2, noise_type='normal', noise_parameters=[0, 0.01], dt=dt)
14+
x, x_truth, dxdt_truth, extras = pi_cruise_control(duration=2, noise_type='normal', noise_parameters=[0, 0.01], dt=dt)
1515
cutoff_frequency = 10 # in Hz
1616
log_gamma = -1.6 * np.log(cutoff_frequency) - 0.71 * np.log(dt) - 5.1
1717
tvgamma = np.exp(log_gamma)
@@ -47,24 +47,6 @@ def test_friedrichsdiff():
4747
assert params1['window_size'] == 9
4848
assert params2['window_size'] == 1
4949

50-
def test_butterdiff():
51-
params1, val1 = optimize(butterdiff, x, dt, search_space={'num_iterations':1}, dxdt_truth=dxdt_truth, maxiter=20, padding='auto')
52-
params2, val2 = optimize(butterdiff, x, dt, search_space={'num_iterations':1}, tvgamma=tvgamma, dxdt_truth=None, maxiter=20, padding='auto')
53-
54-
assert params1['filter_order'] == 8 or params1['filter_order'] == 9
55-
np.testing.assert_almost_equal(params1['cutoff_freq'], 0.161, decimal=3)
56-
assert params2['filter_order'] == 2
57-
np.testing.assert_almost_equal(params2['cutoff_freq'], 0.574, decimal=3)
58-
59-
def test_splinediff():
60-
params1, val1 = optimize(splinediff, x, dt, dxdt_truth=dxdt_truth, padding='auto', maxiter=20)
61-
params2, val2 = optimize(splinediff, x, dt, tvgamma=tvgamma, dxdt_truth=None, padding='auto', maxiter=20)
62-
63-
assert (params1['order'], params1['num_iterations']) == (4, 1)
64-
np.testing.assert_almost_equal(params1['s'], 0.0146, decimal=3)
65-
assert (params2['order'], params2['num_iterations']) == (4, 1)
66-
np.testing.assert_almost_equal(params2['s'], 0.01, decimal=3)
67-
6850
def test_iterative_velocity():
6951
params1, val1 = optimize(iterative_velocity, x, dt, search_space={'num_iterations':1}, dxdt_truth=dxdt_truth, padding='auto')
7052
params2, val2 = optimize(iterative_velocity, x, dt, search_space={'num_iterations':1}, tvgamma=tvgamma, dxdt_truth=None, padding='auto')
@@ -79,7 +61,7 @@ def test_velocity():
7961
params1, val1 = optimize(velocity, x, dt, dxdt_truth=dxdt_truth, padding='auto', maxiter=20)
8062
params2, val2 = optimize(velocity, x, dt, tvgamma=tvgamma, dxdt_truth=None, padding='auto', maxiter=20)
8163

82-
np.testing.assert_almost_equal(params1['gamma'], 0.0721, decimal=3)
64+
np.testing.assert_almost_equal(params1['gamma'], 0.0769, decimal=3)
8365
np.testing.assert_almost_equal(params2['gamma'], 0.010, decimal=3)
8466

8567
def test_acceleration():
@@ -89,19 +71,19 @@ def test_acceleration():
8971
params1, val1 = optimize(acceleration, x, dt, dxdt_truth=dxdt_truth, padding='auto', maxiter=20)
9072
params2, val2 = optimize(acceleration, x, dt, tvgamma=tvgamma, padding='auto', maxiter=20)
9173

92-
np.testing.assert_almost_equal(params1['gamma'], 0.144, decimal=3)
74+
np.testing.assert_almost_equal(params1['gamma'], 0.147, decimal=3)
9375
np.testing.assert_almost_equal(params2['gamma'], 0.0046, decimal=4)
9476

9577
def test_savgoldiff():
9678
params1, val1 = optimize(savgoldiff, x, dt, dxdt_truth=dxdt_truth, padding='auto')
9779
params2, val2 = optimize(savgoldiff, x, dt, tvgamma=tvgamma, dxdt_truth=None, padding='auto')
98-
assert (params1['poly_order'], params1['window_size'], params1['smoothing_win']) == (10, 57, 3)
80+
assert (params1['poly_order'], params1['window_size'], params1['smoothing_win']) == (7, 41, 3)
9981
assert (params2['poly_order'], params2['window_size'], params2['smoothing_win']) == (3, 3, 5)
10082

10183
def test_spectraldiff():
10284
params1, val1 = optimize(spectraldiff, x, dt, dxdt_truth=dxdt_truth, padding='auto')
10385
params2, val2 = optimize(spectraldiff, x, dt, tvgamma=tvgamma, padding='auto')
104-
np.testing.assert_almost_equal(params1['high_freq_cutoff'], 0.105, decimal=3)
86+
np.testing.assert_almost_equal(params1['high_freq_cutoff'], 0.0975, decimal=3)
10587
np.testing.assert_almost_equal(params2['high_freq_cutoff'], 0.10, decimal=2)
10688

10789
def test_polydiff():

pynumdiff/utils/simulate.py

Lines changed: 41 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,13 @@ def linear_autonomous(duration=4, noise_type='normal', noise_parameters=(0, 0.5)
211211
return np.ravel(noisy_x)[1:][idx], smooth_x[1:][idx], dxdt[1:][idx], None
212212

213213

214-
def pi_control(duration=4, noise_type='normal', noise_parameters=(0, 0.5),
214+
def pi_cruise_control(duration=4, noise_type='normal', noise_parameters=(0, 0.5),
215215
random_seed=1, dt=0.01):
216-
"""Create a toy example of linear proportional integral controller with nonlinear control inputs
216+
"""Create a toy example of linear proportional integral controller with nonlinear control inputs.
217+
Simulate proportional integral control of a car attempting to maintain constant velocity while going
218+
up and down hills. We assume the car has arbitrary power and can achieve whatever acceleration it wants;
219+
its mass only factors in via -mg pulling it downhill. This is a linear interpretation of something
220+
similar to what is described in Astrom and Murray 2008 Chapter 3.
217221
218222
:param float duration: governs the length of the series, duration/dt
219223
:param str noise_type: type of noise, compatible with :code:`np.random` functions
@@ -232,83 +236,54 @@ def pi_control(duration=4, noise_type='normal', noise_parameters=(0, 0.5),
232236
- **vel** -- a true derivative information of the time series;
233237
- **[measurements, controls]** -- a list of extra measurements and controls
234238
"""
235-
t = np.arange(0, duration, dt)
236-
237-
actual_vals, extra_measurements, controls = _pi_cruise_control(duration, dt)
238-
x = np.ravel(actual_vals[0, :])
239-
dxdt = np.ravel(actual_vals[1, :])
240-
241-
noisy_x = _add_noise(x, random_seed, noise_type, noise_parameters)
242-
243-
actual_vals = np.array(np.vstack((x, dxdt)))
244-
noisy_measurements = np.array(noisy_x)
245-
246-
noisy_pos = np.ravel(noisy_measurements)
247-
pos = np.ravel(actual_vals[0, :])
248-
vel = np.ravel(actual_vals[1, :])
249-
250-
return noisy_pos, pos, vel, \
251-
[np.array(extra_measurements), np.array(controls)]
252-
253-
254-
def _pi_cruise_control(duration=4, dt=0.01):
255-
"""Simulate proportional integral control of a car attempting to maintain constant velocity while going
256-
up and down hills. This function is used for testing differentiation methods. This is a linear
257-
interpretation of something similar to what is described in Astrom and Murray 2008 Chapter 3.
258-
259-
:param float duration: number of seconds to simulate
260-
:param dt: timestep in seconds
261-
262-
:return: tuple[np.array, np.array, np.array] of arrays of shape (N, M), where M is the
263-
number of time steps\n
264-
- **state_vals** -- state of the car, i.e. position and velocity as a function of time
265-
- **disturbances** -- disturbances from hills that the car is subjected to
266-
- **controls** -- control inputs applied by the car
267-
"""
268-
_np = np # for compatibility with original code
269-
t = _np.arange(0, duration+dt, dt)
270-
271239
# disturbance
272-
hills = _np.sin(2*_np.pi*t) + 0.3*_np.sin(4*2*_np.pi*t + 0.5) + 1.2*_np.sin(1.7*2*_np.pi*t + 0.5)
273-
hills = 0.01*hills
240+
t = np.arange(0, duration, dt)
241+
slope = 0.01*(np.sin(2*np.pi*t) + 0.3*np.sin(4*2*np.pi*t + 0.5) + 1.2*np.sin(1.7*2*np.pi*t + 0.5))
274242

275243
# parameters
276244
mg = 10000 # mass*gravity
277245
fr = 0.9 # friction
278-
ki = 5/0.01*dt # integral control
279-
kp = 25/0.01*dt # proportional control
246+
ki = 0.05 # integral control
247+
kp = 0.25 # proportional control
280248
vd = 0.5 # desired velocity
281249

282-
A = _np.array([[1, dt, 0, 0, 0],
283-
[0, 1, dt, 0, 0],
284-
[0, -fr, 0, -mg, ki],
285-
[0, 0, 0, 0, 0],
286-
[0, 0, 0, 0, 1]])
287-
288-
B = _np.array([[0, 0],
289-
[0, 0],
290-
[0, kp],
291-
[1, 0],
292-
[0, 1]])
250+
# Here state is [pos, vel, accel, cumulative pos error]
251+
A = np.array([[1, dt, (dt**2)/2, 0], # Taylor expand out to accel
252+
[0, 1, dt, 0],
253+
[0, -fr, 0, ki/(dt**2)], # (pos error) / dt^2 puts it in units of accel
254+
[0, 0, 0, 1]])
293255

294-
x0 = _np.array([0, 0, 0, hills[0], 0]).reshape(A.shape[0], 1)
256+
# Here inputs are [slope, vel_desired - vel_estimated]
257+
B = np.array([[0, 0],
258+
[0, 0],
259+
[-mg, kp/dt], # (vel error) / dt puts it in units of accel
260+
[0, dt]])
295261

296262
# run simulation
297-
xs = [x0]
298-
us = [_np.array([0, 0]).reshape([2,1])]
299-
for i in range(1, len(hills)-1):
300-
u = _np.array([hills[i], vd - xs[-1][1,0]]).reshape([2,1])
301-
xnew = A@xs[-1] + B@u
302-
xs.append(xnew)
303-
us.append(u)
263+
states = [np.array([0, 0, 0, 0])] # x0 is all zeros
264+
controls = []
265+
for i in range(len(slope)):
266+
u = np.array([slope[i], vd - states[-1][1]]) # current vel is in 1st position of last state
267+
xnew = A @ states[-1] + B @ u
268+
states.append(xnew)
269+
controls.append(u)
270+
271+
states = np.vstack(states).T
272+
controls = np.vstack(controls).T
273+
274+
x = np.ravel(states[0, :])
275+
dxdt = np.ravel(states[1, :])
276+
277+
noisy_x = _add_noise(x, random_seed, noise_type, noise_parameters)
304278

305-
xs = _np.hstack(xs)
306-
us = _np.hstack(us)
279+
states = np.array(np.vstack((x, dxdt)))
280+
noisy_measurements = np.array(noisy_x)
307281

308-
if len(hills.shape) == 1:
309-
hills = _np.reshape(hills, [1, len(hills)])
282+
noisy_pos = np.ravel(noisy_measurements)
283+
pos = np.ravel(states[0, :])
284+
vel = np.ravel(states[1, :])
310285

311-
return xs, hills, us
286+
return noisy_pos, pos, vel, [slope, controls]
312287

313288

314289
def lorenz_x(duration=4, noise_type='normal', noise_parameters=(0, 0.5),

0 commit comments

Comments
 (0)