Skip to content

Commit b1a8b95

Browse files
committed
Added FWS EKF example
1 parent 59430d6 commit b1a8b95

File tree

1 file changed

+393
-0
lines changed

1 file changed

+393
-0
lines changed

fws_beacons_ekf.py

Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
"""
2+
Example fws_beacons_ekf.py
3+
Author: Joshua A. Marshall <[email protected]>
4+
GitHub: https://github.com/botprof/agv-examples
5+
"""
6+
7+
# %%
8+
# SIMULATION SETUP
9+
10+
import numpy as np
11+
import matplotlib.pyplot as plt
12+
from numpy.linalg import inv
13+
from scipy.stats import chi2
14+
from mobotpy.integration import rk_four
15+
from mobotpy.models import FourWheelSteered
16+
17+
# Set the simulation time [s] and the sample period [s]
18+
SIM_TIME = 10.0
19+
T = 0.04
20+
21+
# Create an array of time values [s]
22+
t = np.arange(0.0, SIM_TIME, T)
23+
N = np.size(t)
24+
25+
# %%
26+
# MOBILE ROBOT SETUP
27+
28+
# Set the wheelbase and track of the vehicle [m]
29+
ELL_W = 2.50
30+
ELL_T = 1.75
31+
32+
# Let's now use the class Ackermann for plotting
33+
vehicle = FourWheelSteered(ELL_W, ELL_T)
34+
35+
# %%
36+
# CREATE A MAP OF FEATURES
37+
38+
# Set a minimum number of features in the map that achieves observability
39+
N_FEATURES = 5
40+
41+
# Set the size [m] of a square map
42+
D_MAP = 50.0
43+
44+
# Create a map of randomly placed feature locations
45+
f_map = np.zeros((2, N_FEATURES))
46+
for i in range(0, N_FEATURES):
47+
f_map[:, i] = D_MAP * (np.random.rand(2) - 0.5)
48+
49+
# %%
50+
# SET UP THE NOISE COVARIANCE MATRICES
51+
52+
# Range sensor noise standard deviation (each feature has the same noise) [m]
53+
SIGMA_W1 = 1.0
54+
55+
# Steering angle noise standard deviation [rad]
56+
SIGMA_W2 = 0.05
57+
58+
# Sensor noise covariance matrix
59+
R = np.zeros((N_FEATURES + 1, N_FEATURES + 1))
60+
R[0:N_FEATURES, 0:N_FEATURES] = np.diag([SIGMA_W1**2] * N_FEATURES)
61+
R[N_FEATURES, N_FEATURES] = SIGMA_W2**2
62+
63+
# Speed noise standard deviation [m/s]
64+
SIGMA_U1 = 0.1
65+
66+
# Steering rate noise standard deviation [rad/s]
67+
SIGMA_U2 = 0.2
68+
69+
# Process noise covariance matrix
70+
Q = np.diag([SIGMA_U1**2, SIGMA_U2**2])
71+
72+
# %%
73+
# FUNCTION TO MODEL RANGE TO FEATURES
74+
75+
76+
def range_sensor(x, sigma_w, f_map):
77+
"""
78+
Function to model the range sensor.
79+
80+
Parameters
81+
----------
82+
x : ndarray
83+
An array of length 2 representing the robot's position.
84+
sigma_w : float
85+
The range sensor noise standard deviation.
86+
f_map : ndarray
87+
An array of size (2, N_FEATURES) containing map feature locations.
88+
89+
Returns
90+
-------
91+
ndarray
92+
The range (including noise) to each feature in the map.
93+
"""
94+
95+
# Compute the measured range to each feature from the current robot position
96+
z = np.zeros(N_FEATURES)
97+
for j in range(0, N_FEATURES):
98+
z[j] = (
99+
np.sqrt((f_map[0, j] - x[0]) ** 2 + (f_map[1, j] - x[1]) ** 2)
100+
+ sigma_w * np.random.randn()
101+
)
102+
103+
# Return the array of noisy measurements
104+
return z
105+
106+
107+
# %%
108+
# FUNCTION TO IMPLEMENT THE EKF FOR THE FWS MOBILE ROBOT
109+
110+
111+
def fws_ekf(q, P, u, z, Q, R, f_map):
112+
"""
113+
Function to implement an observer for the robot's pose.
114+
115+
Parameters
116+
----------
117+
q : ndarray
118+
An array of length 4 representing the robot's pose.
119+
P : ndarray
120+
The state covariance matrix.
121+
u : ndarray
122+
An array of length 2 representing the robot's inputs.
123+
z : ndarray
124+
The range measurements to the features.
125+
Q : ndarray
126+
The process noise covariance matrix.
127+
R : ndarray
128+
The sensor noise covariance matrix.
129+
f_map : ndarray
130+
An array of size (2, N_FEATURES) containing map feature locations.
131+
132+
Returns
133+
-------
134+
ndarray
135+
The estimated state of the robot.
136+
ndarray
137+
The state covariance matrix.
138+
"""
139+
140+
# Compute the a priori estimate
141+
# (a) Compute the Jacobian matrices (i.e., linearize about current estimate)
142+
F = np.zeros((4, 4))
143+
F = np.eye(4) + T * np.array(
144+
[
145+
[
146+
0,
147+
0,
148+
-u[0] * np.sin(q[2]) * np.cos(q[3]),
149+
-u[0] * np.cos(q[2]) * np.sin(q[3]),
150+
],
151+
[
152+
0,
153+
0,
154+
u[0] * np.cos(q[2]) * np.cos(q[3]),
155+
-u[0] * np.sin(q[2]) * np.sin(q[3]),
156+
],
157+
[0, 0, 0, u[0] * 1.0 / (0.5 * ELL_W) * np.cos(q[3])],
158+
[0, 0, 0, 0],
159+
]
160+
)
161+
G = np.zeros((4, 2))
162+
G = T * np.array(
163+
[
164+
[np.cos(q[2]) * np.cos(q[3]), 0],
165+
[np.sin(q[2]) * np.cos(q[3]), 0],
166+
[1.0 / (0.5 * ELL_W) * np.sin(q[3]), 0],
167+
[0, 1],
168+
]
169+
)
170+
171+
# (b) Compute the state covariance matrix and state update
172+
P_new = F @ P @ F.T + G @ Q @ G.T
173+
P_new = 0.5 * (P_new + P_new.T)
174+
q_new = q + T * vehicle.f(q, u)
175+
176+
# Compute the a posteriori estimate
177+
# (a) Compute the Jacobian matrices (i.e., linearize about current estimate)
178+
H = np.zeros((N_FEATURES + 1, 4))
179+
for j in range(0, N_FEATURES):
180+
H[j, :] = np.array(
181+
[
182+
-(f_map[0, j] - q[0]) / range_sensor(q, 0, f_map)[j],
183+
-(f_map[1, j] - q[1]) / range_sensor(q, 0, f_map)[j],
184+
0,
185+
0,
186+
]
187+
)
188+
# Add a measurement for the steering angle
189+
H[N_FEATURES, :] = np.array([0, 0, 0, 1])
190+
191+
# Check the observability of this system
192+
observability_matrix = H
193+
for j in range(1, 4):
194+
observability_matrix = np.concatenate(
195+
(observability_matrix, H @ np.linalg.matrix_power(F, j)), axis=0
196+
)
197+
if np.linalg.matrix_rank(observability_matrix) < 4:
198+
raise ValueError("System is not observable!")
199+
200+
# (b) Compute the Kalman gain
201+
K = P_new @ H.T @ inv(H @ P_new @ H.T + R)
202+
203+
# (c) Compute the state update
204+
z_hat = np.zeros(N_FEATURES + 1)
205+
z_hat[0:N_FEATURES] = range_sensor(q_new, 0, f_map)
206+
z_hat[N_FEATURES] = q_new[3]
207+
q_new = q_new + K @ (z - z_hat)
208+
209+
# (d) Compute the covariance update
210+
P_new = (np.eye(4) - K @ H) @ P_new @ (np.eye(4) - K @ H).T + K @ R @ K.T
211+
P_new = 0.5 * (P_new + P_new.T)
212+
213+
# Return the estimated state
214+
return q_new, P_new
215+
216+
217+
# %%
218+
# RUN SIMULATION
219+
220+
# Initialize arrays that will be populated with our inputs and states
221+
x = np.zeros((4, N))
222+
u = np.zeros((2, N))
223+
x_hat = np.zeros((4, N))
224+
P_hat = np.zeros((4, 4, N))
225+
226+
# Set the initial pose [m, m, rad, rad], velocities [m/s, rad/s]
227+
x[0, 0] = -2.0
228+
x[1, 0] = 3.0
229+
x[2, 0] = np.pi/8.0
230+
x[3, 0] = 0.0
231+
u[0, 0] = 2.0
232+
u[1, 0] = 0
233+
234+
# Initialize the state estimate
235+
x_hat[:, 0] = np.array([0.0, 0.0, 0.0, 0.0])
236+
P_hat[:, :, 0] = np.diag([5.0**2, 5.0**2, (np.pi/4)**2, 0.05**2])
237+
238+
# Just drive around and try to localize!
239+
for k in range(1, N):
240+
# Simulate the robot's motion
241+
x[:, k] = rk_four(vehicle.f, x[:, k - 1], u[:, k - 1], T)
242+
# Measure the actual range to each feature
243+
z = np.zeros(N_FEATURES + 1)
244+
z[0:N_FEATURES] = range_sensor(x[:, k], SIGMA_W1, f_map)
245+
z[N_FEATURES] = x[3, k] + SIGMA_W2 * np.random.randn()
246+
247+
# Use the range measurements to estimate the robot's state
248+
x_hat[:, k], P_hat[:, :, k] = fws_ekf(
249+
x_hat[:, k - 1], P_hat[:, :, k - 1], u[:, k - 1], z, Q, R, f_map
250+
)
251+
# Choose some new inputs
252+
# Choose some new inputs
253+
u[0, k] = 2.0
254+
u[1, k] = -0.1 * np.sin(1 * t[k])
255+
256+
# %%
257+
# MAKE SOME PLOTS
258+
259+
260+
# Function to wrap angles to [-pi, pi]
261+
def wrap_to_pi(angle):
262+
"""Wrap angles to the range [-pi, pi]."""
263+
return (angle + np.pi) % (2 * np.pi) - np.pi
264+
265+
266+
# Change some plot settings (optional)
267+
plt.rc("text", usetex=True)
268+
plt.rc("text.latex", preamble=r"\usepackage{cmbright,amsmath,bm}")
269+
plt.rc("savefig", format="pdf")
270+
plt.rc("savefig", bbox="tight")
271+
272+
# Find the scaling factor for plotting covariance bounds
273+
ALPHA = 0.01
274+
s1 = chi2.isf(ALPHA, 1)
275+
s2 = chi2.isf(ALPHA, 2)
276+
277+
# Plot the position of the vehicle in the plane
278+
fig1 = plt.figure(1)
279+
plt.plot(f_map[0, :], f_map[1, :], "C4*", label="Feature")
280+
plt.plot(x[0, :], x[1, :], "C0", label="Actual")
281+
plt.plot(x_hat[0, :], x_hat[1, :], "C1--", label="Estimated")
282+
plt.axis("equal")
283+
X_BL, Y_BL, X_BR, Y_BR, X_FL, Y_FL, X_FR, Y_FR, X_BD, Y_BD = vehicle.draw(x[:, 0])
284+
plt.fill(X_BL, Y_BL, "k")
285+
plt.fill(X_BR, Y_BR, "k")
286+
plt.fill(X_FR, Y_FR, "k")
287+
plt.fill(X_FL, Y_FL, "k")
288+
plt.fill(X_BD, Y_BD, "C2", alpha=0.5, label="Start")
289+
X_BL, Y_BL, X_BR, Y_BR, X_FL, Y_FL, X_FR, Y_FR, X_BD, Y_BD = vehicle.draw(x[:, N - 1])
290+
plt.fill(X_BL, Y_BL, "k")
291+
plt.fill(X_BR, Y_BR, "k")
292+
plt.fill(X_FR, Y_FR, "k")
293+
plt.fill(X_FL, Y_FL, "k")
294+
plt.fill(X_BD, Y_BD, "C3", alpha=0.5, label="End")
295+
plt.xlabel(r"$x$ [m]")
296+
plt.ylabel(r"$y$ [m]")
297+
plt.legend()
298+
299+
# Plot the states as a function of time
300+
fig2 = plt.figure(2)
301+
fig2.set_figheight(6.4)
302+
ax2a = plt.subplot(411)
303+
plt.plot(t, x[0, :], "C0", label="Actual")
304+
plt.plot(t, x_hat[0, :], "C1--", label="Estimated")
305+
plt.grid(color="0.95")
306+
plt.ylabel(r"$x$ [m]")
307+
plt.setp(ax2a, xticklabels=[])
308+
plt.legend()
309+
ax2b = plt.subplot(412)
310+
plt.plot(t, x[1, :], "C0", label="Actual")
311+
plt.plot(t, x_hat[1, :], "C1--", label="Estimated")
312+
plt.grid(color="0.95")
313+
plt.ylabel(r"$y$ [m]")
314+
plt.setp(ax2b, xticklabels=[])
315+
ax2c = plt.subplot(413)
316+
plt.plot(t, wrap_to_pi(x[2, :]) * 180.0 / np.pi, "C0", label="Actual")
317+
plt.plot(t, wrap_to_pi(x_hat[2, :]) * 180.0 / np.pi, "C1--", label="Estimated")
318+
plt.ylabel(r"$\theta$ [deg]")
319+
plt.grid(color="0.95")
320+
plt.setp(ax2c, xticklabels=[])
321+
ax2d = plt.subplot(414)
322+
plt.plot(t, wrap_to_pi(x[3, :]) * 180.0 / np.pi, "C0", label="Actual")
323+
plt.plot(t, wrap_to_pi(x_hat[3, :]) * 180.0 / np.pi, "C1--", label="Estimated")
324+
plt.ylabel(r"$\phi$ [deg]")
325+
plt.grid(color="0.95")
326+
plt.xlabel(r"$t$ [s]")
327+
328+
# Plot the estimator errors as a function of time
329+
sigma = np.zeros((4, N))
330+
fig3 = plt.figure(3)
331+
fig3.set_figheight(6.4)
332+
ax3a = plt.subplot(411)
333+
sigma[0, :] = np.sqrt(s1 * P_hat[0, 0, :])
334+
plt.fill_between(
335+
t,
336+
-sigma[0, :],
337+
sigma[0, :],
338+
color="C0",
339+
alpha=0.2,
340+
label=str(100 * (1 - ALPHA)) + r" \% Confidence",
341+
)
342+
plt.plot(t, x[0, :] - x_hat[0, :], "C0", label="Error")
343+
plt.grid(color="0.95")
344+
plt.ylabel(r"$x$ [m]")
345+
plt.setp(ax2a, xticklabels=[])
346+
plt.legend()
347+
ax3b = plt.subplot(412)
348+
sigma[1, :] = np.sqrt(s1 * P_hat[1, 1, :])
349+
plt.fill_between(
350+
t,
351+
-sigma[1, :],
352+
sigma[1, :],
353+
color="C0",
354+
alpha=0.2,
355+
label=str(100 * (1 - ALPHA)) + r" \% Confidence",
356+
)
357+
plt.plot(t, x[1, :] - x_hat[1, :], "C0", label="Error")
358+
plt.grid(color="0.95")
359+
plt.ylabel(r"$y$ [m]")
360+
plt.setp(ax2b, xticklabels=[])
361+
ax3c = plt.subplot(413)
362+
sigma[2, :] = np.sqrt(s1 * P_hat[2, 2, :])
363+
plt.fill_between(
364+
t,
365+
-sigma[2, :] * 180.0 / np.pi,
366+
sigma[2, :] * 180.0 / np.pi,
367+
color="C0",
368+
alpha=0.2,
369+
label=str(100 * (1 - ALPHA)) + r" \% Confidence",
370+
)
371+
plt.plot(t, (x[2, :] - x_hat[2, :]) * 180.0 / np.pi, "C0", label="Error")
372+
plt.ylabel(r"$\theta$ [deg]")
373+
plt.grid(color="0.95")
374+
plt.setp(ax2c, xticklabels=[])
375+
ax3d = plt.subplot(414)
376+
sigma[3, :] = np.sqrt(s1 * P_hat[3, 3, :])
377+
plt.fill_between(
378+
t,
379+
-sigma[3, :] * 180.0 / np.pi,
380+
sigma[3, :] * 180.0 / np.pi,
381+
color="C0",
382+
alpha=0.2,
383+
label=str(100 * (1 - ALPHA)) + r" \% Confidence",
384+
)
385+
plt.plot(t, (x[3, :] - x_hat[3, :]) * 180 / np.pi, "C0", label="Error")
386+
plt.ylabel(r"$\phi$ [deg]")
387+
plt.grid(color="0.95")
388+
plt.xlabel(r"$t$ [s]")
389+
390+
# Show all the plots to the screen
391+
plt.show()
392+
393+
# %%

0 commit comments

Comments
 (0)