2.8 Steering a Mobile Robot
linear algebra for robot control
1Learning Objectives¶
By the end of this page, you should know:
- how to represent robot dynamics using matrices
- how to express possible solutions to steer to goal a robot using the null space of a dynamics matrix
2Mobile Robot Description¶
Our goal will be to steer a mobile robot to a desired position. Examples of mobile robots are shown below. On the left is a mobile robot research platform used by GRASP lab, while on the right is a roomba vaccuum cleaner.
We first develop a model of the mobile robot. We assume that the robot moves in with its -position denoted and -position denoted . We gather these two coordinates together and write them as a vector
which is typically called the state of the robot. Because we are modeling a mobile robot, its position/state changes with time. We discretize time into uniform samples. So, we let be some base unit or duration of time, typically small, and define , where . We denote the robot’s position at time by
in other words, the subscript keeps track of time. With this notation, will be the initial position of the robot in the plane (i.e., in ) at time .
The next thing we posit is that our mobile robot has “dynamics”, meaning that its state at time can be expressed as a function of its state at time and any motor commands that we provide. A reasonably accurate model for the motion of a mobile robot can be captured by
where is a pair of motor commands, the matrix distributes the motor commands to effect motion of the robot (that is, change its next position), and is a matrix that in a realistic model would capture the mass of the robot, the inertia of rotating parts, and other effects from physics. Even though the following values are not so realistic, we’ll go ahead and assume that
so that we can focus on the process of steering the robot and not the complexity of the robot’s model.
Not only do we have a way to compute the state at the next time instant based on the current state and current input, we can also iterate forward and predict the state at some future time, say , as a function of a hypothesized input sequence, as follows:
That’s a lot of symbols, but what do they tell us? Suppose we start the robot at and we set the motor commands to be identically zero. Then we can predict how the robot will move “on its own”. Below, we consider how the robot moves for 20 seconds (200 timesteps due to the timestep of seconds) without any motor commands.
import numpy as np
import matplotlib.pyplot as plt
dt = 0.1
A = np.eye(2) + dt*np.array([[0, -0.5],[0.5, 0.0]])
B = dt*np.eye(2)
def simulate_robot(A,B, N, inputs, p_0):
positions = np.zeros((N+1, 2))
positions[0] = p_0
for k in range(N):
positions[k+1] = A@positions[k] + B@inputs[k]
return positions
N = 200 #20 seconds with timestep of 0.1
inputs = np.zeros((N,2))
p_0 = np.array([1.0, 1.0])
positions = simulate_robot(A,B,N, inputs, p_0)
plt.plot(positions[:,0], positions[:,1])
plt.xlabel(r'$p_x$')
plt.ylabel(r'$p_y$')
plt.plot(positions[0,0], positions[0,1], 'go')
plt.arrow(positions[0,0], positions[0,1], -5*(positions[0,0] - positions[1,0]), -5*(positions[0,0] - positions[1,1]), head_width=.1, color='green')
plt.plot(positions[-1,0], positions[-1,1], 'ro')
plt.arrow(positions[-8,0], positions[-8,1], -.6*(positions[-8,0] - positions[-1,0]), -.6*(positions[-8,1] - positions[-1,1]), head_width=.1, color='red')
3Steering the robot¶
It seems that the robot is spiraling out aimlessly from it’s initial position. Let’s try to add some direction to the robot. We will try to steer the robot from its initial position to the origin in two seconds. In particular, we will set . Our goal is to steer the robot to the final position
from the initial position
Then we are seeking a solution to the equation
where the unknowns are . This can be rearranged as
Let us first make sure that the problem is well-posed, i.e. that there exists at least one sequence of inputs that steers us from to . This can be verified by checking that the vector is in the column space of . By the fact that the rightmost entry in this block matrix is , we can set the sequence of inputs as , and . With this choice, the equation (1) is satisfied. Therefore, there exists at least one solution. Let’s check out that solution below.
N = 20 #2 seconds with timestep of 0.1
simple_inputs = np.zeros((N,2))
p_0 = np.array([1.0, 1.0])
p_N = np.zeros(2)
simple_inputs[-1] = 1/(0.1)*(p_N - np.linalg.matrix_power(A, N)@p_0)
positions = simulate_robot(A,B,N, simple_inputs, p_0)
plt.plot(positions[:,0], positions[:,1])
plt.xlabel(r'$p_x$')
plt.ylabel(r'$p_y$')
plt.plot(positions[0,0], positions[0,1], 'go')
plt.arrow(positions[0,0], positions[0,1], -5*(positions[0,0] - positions[1,0]), -5*(positions[0,0] - positions[1,1]), head_width=.1, color='green')
plt.plot(positions[-1,0], positions[-1,1], 'ro')
plt.arrow(positions[-3,0], positions[-3,1], -.6*(positions[-3,0] - positions[-1,0]), -.6*(positions[-3,1] - positions[-1,1]), head_width=.1, color='red')
We see that the robot begins by following it’s natural dynamics. Then in the last 0.1 seconds, the inputs supply a command that pushes the robot to the origin. Let’s examine the commands.
print('input command: ', simple_inputs)
input command: [[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 0. 0. ]
[ 3.07604237 -14.16965292]]
4Alternative solutions¶
That’s a big jump! We’re supply no commands most of the time, and then applying a large input at the last possible opportunity.
Let’s try expressing other possible solutions to (1). We know that the above solution works. Let us call that solution . We are looking for other possible solutions such that
As
we may substitute for the left hand side of (10). Then we are looking for commands satisfying the following equation.
Rearranging, we are looking for vectors such that
which implies that must be in the null space of . This recovers the conclusion of this theorem. In particular, all solutions to (10) are given by , where is any element in the nullspace of . Let’s try randomly selecting a element from the nullspace, and using that to steer our robot. To select an element, we can first obtain a basis for the nullspace, as in this example. Fortunately, scipy has provides us a way to compute such an element automatically by calling the function scipy.linalg.null_space.
import scipy.linalg
stacked_dynamics = np.hstack([np.linalg.matrix_power(A, n)@B for n in range(N-1, -1, -1)])
null_basis = scipy.linalg.null_space(stacked_dynamics)
print('Null space dimension: ', null_basis.shape[1])
Null space dimension: 38
We see that the dimension of the null space is 38. We can therefore take an arbitrary 38 dimensional vector and map it to an element in the null space. We will do this below, and define our sequence of inputs using the resulting vector.
v = np.random.randn(38)
n = null_basis@v
inputs = simple_inputs + n.reshape((N,2))
positions = simulate_robot(A,B,N, inputs, p_0)
plt.plot(positions[:,0], positions[:,1])
plt.xlabel(r'$p_x$')
plt.ylabel(r'$p_y$')
plt.plot(positions[0,0], positions[0,1], 'go')
plt.arrow(positions[0,0], positions[0,1], -5*(positions[0,0] - positions[1,0]), -5*(positions[0,0] - positions[1,1]), head_width=.1, color='green')
plt.plot(positions[-1,0], positions[-1,1], 'ro')
plt.arrow(positions[-3,0], positions[-3,1], -.6*(positions[-3,0] - positions[-1,0]), -.6*(positions[-3,1] - positions[-1,1]), head_width=.1, color='red')
5Adding Waypoints¶
Well... we still get to the destination, but we’ve now taken a weird path because of our null space element was chosen randomly. To take a more reasonable path, we could instead try to add more desired midway points to pass through. Such points are called waypoints. Let’s select a sequence of 4 waypoints, evenly spaced over the 2 seconds. As we have total timesteps, this means that we have a desired location for and . We will set these waypoints as follows:
Using the expression for the dynamics from equation (5), we have that for any ,
We may therefore stack up our waypoint constraints as
As before, we can express our solution as any solution plus , where
To find a vector to parametrize this solution, we may use an approach simliar to the one taken to solve . In particular, we may set
For all other , may be set to zero. The difference from our solution to finding is that it was necessary to account for the impact of previous inputs on the dynamics when searching for the subsequent inputs. We check this solution by simulating the system below. We will set .
import numpy as np
import matplotlib.pyplot as plt
waypoint_inputs = np.zeros((N,2))
p_0 = np.array([1.0, 1.0])
waypoints = np.array([[0.75, 0.75], [0.5, 0.5], [0.25, 0.25], [0.0, 0.0]])
waypoint_inputs[4] = 1/dt*(waypoints[0] - np.linalg.matrix_power(A, 5)@p_0)
waypoint_inputs[9] = 1/dt*(waypoints[1] - np.linalg.matrix_power(A, 10)@p_0 - np.linalg.matrix_power(A, 5)@B@waypoint_inputs[4])
waypoint_inputs[14] = 1/dt*(waypoints[2] - np.linalg.matrix_power(A, 15)@p_0- np.linalg.matrix_power(A, 5)@B@waypoint_inputs[9]
- np.linalg.matrix_power(A, 10)@B@waypoint_inputs[4])
waypoint_inputs[19] = 1/dt*(waypoints[3] - np.linalg.matrix_power(A, 20)@p_0- np.linalg.matrix_power(A, 5)@B@waypoint_inputs[14]
- np.linalg.matrix_power(A, 10)@B@waypoint_inputs[9] - np.linalg.matrix_power(A, 15)@B@waypoint_inputs[4])
positions = simulate_robot(A,B,N, waypoint_inputs, p_0)
plt.plot(positions[:,0], positions[:,1])
plt.xlabel(r'$p_x$')
plt.ylabel(r'$p_y$')
plt.plot(positions[0,0], positions[0,1], 'go')
plt.plot(waypoints[0,0], waypoints[0,1], '*', color='black')
plt.plot(waypoints[1,0], waypoints[1,1], '*', color='black')
plt.plot(waypoints[2,0], waypoints[2,1], '*', color='black')
plt.arrow(positions[0,0], positions[0,1], -1*(positions[0,0] - positions[1,0]), -1*(positions[0,0] - positions[1,1]), head_width=.05, color='green')
plt.plot(positions[-1,0], positions[-1,1], 'ro')
plt.arrow(positions[-3,0], positions[-3,1], -.6*(positions[-3,0] - positions[-1,0]), -.6*(positions[-3,1] - positions[-1,1]), head_width=.04, color='red')
Our commands still take us to the solution, but now we pass through the selected waypoints during the journey. Adding the constraints that our trajectory passes through the waypoints reduces the degrees of freedom for selecting . Below, we see that the dimension of the nullspace has now been reduce from 38 to 32, as we have added three more constraints, each with two equations.
stacked_dynamics = np.zeros((8, 40))
for i, k in enumerate([5, 10, 15, 20]):
stacked_dynamics_k = np.hstack([np.linalg.matrix_power(A, n)@B for n in range(k-1, -1, -1)])
stacked_dynamics[2*i:(2*i+2), :stacked_dynamics_k.shape[1]] = stacked_dynamics_k
null_basis = scipy.linalg.null_space(stacked_dynamics)
print('Null space dimension: ', null_basis.shape[1])
Null space dimension: 32
6Minimum Energy Control¶
A key question remaining is how we should choose . Setting it to zero leads to somewhat strange input sequences, where we only supply inputs at the time before we need to satisfy a waypoint constraint. Conversely, choosing it randomly leads to very strange behavior. To get more reasonable trajectories, we can consider trying to design the sequence of inputs that minimizes the enegy expenditure of the robot. The energy used by a robot that takes the sequence of actions is given by the sum of squared elements in the input sequence. In particular,
where and are the first and second commands of the input , respectively.
We can therefore seek to find the minimum energy input sequence over the free variables
where the columns of are a basis for
An optimization problem of this form is known as a least squares problem with linear constraints. We won’t go over how to find the solution to such a problem in this course, but it is covered in MATH 1410. It turns out that the solutions is given by
Let’s test this solution out below. Recall that we already solved for a matrix whose columns are a basis for the null space in the above code cell, and the matrix is called ``null_basis’'.
x_star = -np.linalg.inv(null_basis.T@null_basis)@null_basis.T@(waypoint_inputs.flatten())
n = null_basis@x_star
inputs = waypoint_inputs + n.reshape((N,2))
positions = simulate_robot(A,B,N, inputs, p_0)
plt.plot(positions[:,0], positions[:,1])
plt.xlabel(r'$p_x$')
plt.ylabel(r'$p_y$')
plt.plot(positions[0,0], positions[0,1], 'go')
plt.plot(waypoints[0,0], waypoints[0,1], '*', color='black')
plt.plot(waypoints[1,0], waypoints[1,1], '*', color='black')
plt.plot(waypoints[2,0], waypoints[2,1], '*', color='black')
plt.arrow(positions[0,0], positions[0,1], -1*(positions[0,0] - positions[1,0]), -1*(positions[0,0] - positions[1,1]), head_width=.05, color='green')
plt.plot(positions[-1,0], positions[-1,1], 'ro')
plt.arrow(positions[-3,0], positions[-3,1], -.6*(positions[-3,0] - positions[-1,0]), -.6*(positions[-3,1] - positions[-1,1]), head_width=.04, color='red')
Much better! By expressing the evolution of the robot’s dynamics as a matrix, enforcing several waypoints that we want the robot to pass through, and using the minimum energy control sequence to steer the robot through all of the waypoints, we get a much smoother trajectory. Similar approaches can be taken for planning the motor commands in more complex robots such as quadrotors. You will see more about this if you take robotics course like MEAM 6200!