|
| 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