diff --git a/guided_mrmp/controllers/mpc.py b/guided_mrmp/controllers/mpc.py index dc08415154d4e7dafabbd36723f9d7e99de71375..ba3f77b6685bd4f13ebfbfdb0a44c6f6790968ca 100644 --- a/guided_mrmp/controllers/mpc.py +++ b/guided_mrmp/controllers/mpc.py @@ -1,6 +1,6 @@ import numpy as np # from guided_mrmp.utils import Roomba -from guided_mrmp.optimizer import Optimizer +from guided_mrmp.optimizers.optimizer import Optimizer, iLQR np.seterr(divide="ignore", invalid="ignore") @@ -23,17 +23,6 @@ class MPC: self.nx = model.state_dimension() # number of state vars self.nu = model.control_dimension() # number of input/control vars - if len(state_cost) != self.nx: - raise ValueError(f"State Error cost matrix should be of size {self.nx}") - if len(final_state_cost) != self.nx: - raise ValueError(f"End State Error cost matrix should be of size {self.nx}") - if len(input_cost) != self.nu: - raise ValueError(f"Control Effort cost matrix should be of size {self.nu}") - if len(input_rate_cost) != self.nu: - raise ValueError( - f"Control Effort Difference cost matrix should be of size {self.nu}" - ) - self.robot_model = model self.dt = DT @@ -52,127 +41,93 @@ class MPC: self.P = np.diag(input_rate_cost) # Instantiate the optimizer - self.optimizer = Optimizer(self.nx, self.nu, self.control_horizon, self.Q, self.Qf, self.R, self.P) + self.optimizer = iLQR(self.nx, self.nu, self.control_horizon) - - def get_linear_model_matrices_roomba(self,x_bar,u_bar): + def cost_function(self, x, u, target): """ - Computes the approximated LTI state space model x' = Ax + Bu + C - - Args: - x_bar (array-like): State vector [x, y, theta] - u_bar (array-like): Input vector [v, omega] - + Calculate the cost function for the optimizer. + Parameters: + - target: (numpy.ndarray) The target values for tracking. + - u: (numpy.ndarray) The control inputs. + - x: (numpy.ndarray) The state variables. Returns: - A_lin, B_lin, C_lin: Linearized state-space matrices + - cost: (float) The calculated cost. """ + cost = 0 - x = x_bar[0] - y = x_bar[1] - theta = x_bar[2] + - v = u_bar[0] - omega = u_bar[1] + # Tracking error cost + for k in range(self.control_horizon): + cost += opt.quad_form(x[:, k + 1] - target[:, k], self.Q) - ct = np.cos(theta) - st = np.sin(theta) + # Final point tracking cost + cost += opt.quad_form(x[:, -1] - target[:, -1], self.Qf) - # Initialize matrix A with zeros and fill in appropriate elements - A = np.zeros((self.nx, self.nx)) - A[0, 2] = -v * st - A[1, 2] = v * ct + # Actuation magnitude cost + for k in range(self.control_horizon): + cost += opt.quad_form(u[:, k], self.R) - # Discrete-time state matrix A_lin - A_lin = np.eye(self.nx) + self.dt * A + # Actuation rate of change cost + for k in range(1, self.control_horizon): + cost += opt.quad_form(u[:, k] - u[:, k - 1], self.P) - # Initialize matrix B with zeros and fill in appropriate elements - B = np.zeros((self.nx, self.nu)) - B[0, 0] = ct - B[1, 0] = st - B[2, 1] = 1 + return cost - # Discrete-time input matrix B_lin - B_lin = self.dt * B + def constraints(self, initial_state, A, B,C, x, u, robot_model, dt, prev_cmd): - # Compute the non-linear state update equation f(x, u) - f_xu = np.array([v * ct, v * st, omega]).reshape(self.nx, 1) + constr = [] + + # Kinematics Constraints + for k in range(self.control_horizon): + constr += [x[:, k + 1] == A @ x[:, k] + B @ u[:, k] + C] - # Compute the constant vector C_lin - C_lin = (self.dt * (f_xu - np.dot(A, x_bar.reshape(self.nx, 1)) - np.dot(B, u_bar.reshape(self.nu, 1))).flatten()) + # initial state + constr += [x[:, 0] == initial_state] - return A_lin, B_lin, C_lin - - def get_linear_model_matrices(self, x_bar, u_bar): - """ - Computes the approximated LTI state space model x' = Ax + Bu + C + # actuation bounds + constr += [opt.abs(u[:, 0]) <= robot_model.max_acc] + constr += [opt.abs(u[:, 1]) <= robot_model.max_steer] - Args: - x_bar (array-like): - u_bar (array-like): + # Actuation rate of change bounds + constr += [opt.abs(u[0, 0] - prev_cmd[0]) / dt <= robot_model.max_d_acc] + constr += [opt.abs(u[1, 0] - prev_cmd[1]) / dt <= robot_model.max_d_steer] + for k in range(1, self.control_horizon): + constr += [opt.abs(u[0, k] - u[0, k - 1]) / dt <= robot_model.max_d_acc] + constr += [opt.abs(u[1, k] - u[1, k - 1]) / dt <= robot_model.max_d_steer] - Returns: + return constr + def step(self, initial_state, target, prev_cmd, initial_guess=None): """ - - x = x_bar[0] - y = x_bar[1] - v = x_bar[2] - theta = x_bar[3] - - a = u_bar[0] - delta = u_bar[1] - - ct = np.cos(theta) - st = np.sin(theta) - cd = np.cos(delta) - td = np.tan(delta) - - A = np.zeros((self.nx, self.nx)) - A[0, 2] = ct - A[0, 3] = -v * st - A[1, 2] = st - A[1, 3] = v * ct - A[3, 2] = v * td / self.robot_model.wheelbase - A_lin = np.eye(self.nx) + self.dt * A - - B = np.zeros((self.nx, self.nu)) - B[2, 0] = 1 - B[3, 1] = v / (self.robot_model.wheelbase * cd**2) - B_lin = self.dt * B - - f_xu = np.array([v * ct, v * st, a, v * td / self.robot_model.wheelbase]).reshape( - self.nx, 1 - ) - C_lin = ( - self.dt - * ( - f_xu - - np.dot(A, x_bar.reshape(self.nx, 1)) - - np.dot(B, u_bar.reshape(self.nu, 1)) - ).flatten() - ) - return A_lin, B_lin, C_lin - - def step(self, initial_state, target, prev_cmd): - A, B, C = self.get_linear_model_matrices_roomba(initial_state, prev_cmd) # Use Roomba model + Sets up and solves the optimization problem. + + Args: + initial_state: Current estimate of [x, y, heading] + target: State space reference, in the same frame as the provided current state + prev_cmd: Previous [v, delta] + A, B, C: Linearized state-space matrices + initial_guess: Optional initial guess for the optimizer + + Returns: + x_opt: Optimal state trajectory + u_opt: Optimal control trajectory + """ + A,B,C = self.robot_model.linearize(initial_state, prev_cmd, self.dt) + + # set up variables for the optimization problem + x = opt.Variable((self.nx, self.control_horizon + 1)) + u = opt.Variable((self.nu, self.control_horizon)) + + # Define the cost function + cost = self.cost_function(x, u, target) + + # Define the constraints + constraints = self.constraints(initial_state, A, B, C, x, u, self.robot_model, self.dt, prev_cmd) # Use the Optimizer class to solve the optimization problem - x_opt, u_opt = self.optimizer.solve(initial_state, target, prev_cmd, A, B, C, self.robot_model, self.dt) + x_opt, u_opt = self.optimizer.solve(x,u,cost, constraints, initial_guess) return x_opt, u_opt -if __name__ == "__main__": - # Example usage: - dt = 0.1 - roomba = Roomba() - Q = [20, 20, 20] # state error cost - Qf = [30, 30, 30] # state final error cost - R = [10, 10] # input cost - P = [10, 10] # input rate of change cost - mpc = MPC(roomba, 5, dt, Q, Qf, R, P) - x_bar = np.array([0.0, 0.0, 0.0]) - u_bar = np.array([1.0, 0.1]) - A_lin, B_lin, C_lin = mpc.get_linear_model_matrices_roomba(x_bar, u_bar) - print(A_lin) - print(B_lin) - print(C_lin) \ No newline at end of file + diff --git a/guided_mrmp/optimizers/optimizer.py b/guided_mrmp/optimizers/optimizer.py new file mode 100644 index 0000000000000000000000000000000000000000..d793675399525b92111933b51487edbbdab124a5 --- /dev/null +++ b/guided_mrmp/optimizers/optimizer.py @@ -0,0 +1,63 @@ +import cvxpy as opt +import numpy as np + +class Optimizer: + def __init__(self): + pass + + def solve(self, cost_function, constraints, initial_guess): + """ + Solve the optimization problem. + + Parameters: + - cost_function: Function to minimize + - constraints: List of constraints + - initial_guess: Initial guess for the optimizer + + Returns: + - optimal_solution: The optimal control input found by the optimizer + """ + raise NotImplementedError("This method should be implemented by a specific optimizer.") + +class iLQR(Optimizer): + def __init__(self, nx, nu, control_horizon): + self.nx = nx + self.nu = nu + self.control_horizon = control_horizon + + def solve(self, x,u,cost, constraints, initial_guess=None): + """ + Solve the optimization problem. + + Parameters: + - cost_function: Function to minimize + - constraints: List of constraints + - initial_guess: Optional initial guess for the optimizer + + Returns: + - x_opt: Optimal state trajectory + - u_opt: Optimal control trajectory + """ + # Set up variables for the optimization problem + # x = opt.Variable((self.nx, self.control_horizon + 1)) + # u = opt.Variable((self.nu, self.control_horizon)) + + # If initial guess is provided, set the initial values + # if initial_guess: + # x.value = initial_guess['x'] + # u.value = initial_guess['u'] + + + + prob = opt.Problem(opt.Minimize(cost), constraints) + + # Solve the problem + prob.solve(solver=opt.OSQP, warm_start=True, verbose=False) + + if prob.status != opt.OPTIMAL: + raise ValueError("The optimization problem did not solve successfully.") + + # print(f"x val = {x.value}") + # print(f"u val = {u.value}") + + return x.value, u.value \ No newline at end of file diff --git a/guided_mrmp/simulator.py b/guided_mrmp/simulator.py index 6a92cbdaa48ca756f0942ca235fc7880a5e59ba5..ae2171e3ec0a9c3d76e0ca6dbafdb90b91e0d697 100644 --- a/guided_mrmp/simulator.py +++ b/guided_mrmp/simulator.py @@ -1,13 +1,6 @@ import pygame import numpy as np -import os -import random -import matplotlib.pyplot as plt -from guided_mrmp.utils import Env, Roomba, Robot -from guided_mrmp.planners import RRT -from guided_mrmp.planners import RRTStar -from guided_mrmp.planners.multirobot.db_guided_mrmp import GuidedMRMP class Simulator: def __init__(self, robots, dynamics_models, env, policy, settings): diff --git a/guided_mrmp/utils/control.py b/guided_mrmp/utils/control.py index a53d0409918fc67357b3ecf82cdb5034dff3b2b5..c27a4dfcdc0cae582e86f798b68a133d7acadf67 100644 --- a/guided_mrmp/utils/control.py +++ b/guided_mrmp/utils/control.py @@ -1,4 +1,6 @@ import numpy as np +from autograd import jacobian +from autograd.numpy import cos, sin class ControlSpace: """ @@ -25,6 +27,9 @@ class ControlSpace: """Returns a sequence of controls that connects x to y, if applicable. Return None if no such sequence exists.""" return None + def linearize(self, x_bar, u_bar, dt): + """Compute linearized dynamics around (x_bar, u_bar). Returns A_lin, B_lin, C_lin.""" + raise NotImplementedError() class Roomba(ControlSpace): """ @@ -51,12 +56,14 @@ class Roomba(ControlSpace): State x: [x, y, theta] Control u: [v, steering_angle] """ + x_pos, y_pos, theta = x v, steering_angle = u + # Dynamics equations - dx_pos = v * np.cos(theta) - dy_pos = v * np.sin(theta) + dx_pos = v * cos(theta) + dy_pos = v * sin(theta) dtheta = steering_angle return np.array([dx_pos, dy_pos, dtheta]) @@ -76,3 +83,76 @@ class Roomba(ControlSpace): # Return the predicted next state return np.array([new_x, new_y, new_theta]) + + def linearize(self, x_bar, u_bar, dt): + """ + Linearize the system dynamics around (x_bar, u_bar) and discretize them. + + Returns: + A_lin, B_lin, C_lin: Discrete-time linearized state-space matrices + """ + x_bar = np.array(x_bar, dtype=np.float64) + u_bar = np.array(u_bar, dtype=np.float64) + + # Compute continuous-time Jacobians + A_continuous, B_continuous = self.compute_jacobian(x_bar, u_bar) + + print(f"A_continuous: {A_continuous}") + print(f"B_continuous: {B_continuous}") + + + # Discretize the system using Euler approximation + A_lin = np.eye(self.state_dimension()) + A_continuous * dt + B_lin = B_continuous * dt + + # Compute C_lin + f_xu = self.derivative(x_bar, u_bar) + C_lin = dt * (f_xu - A_continuous @ x_bar - B_continuous @ u_bar) + + return A_lin, B_lin, C_lin + + # def compute_jacobian(self, x_bar, u_bar): + # """ + # Compute the Jacobian matrices A and B at (x_bar, u_bar). + + # Returns: + # A: Jacobian of dynamics with respect to the state x + # B: Jacobian of dynamics with respect to the control input u + # """ + # # Use autograd's jacobian function + # A_func = jacobian(lambda x: self.derivative(x, u_bar)) + # B_func = jacobian(lambda u: self.derivative(x_bar, u)) + + # # Evaluate Jacobians at (x_bar, u_bar) + # A = A_func(x_bar) + # B = B_func(u_bar) + + # return A, B + + def compute_jacobian(self, x, u): + """ + Compute the Jacobian matrices A and B based on analytical derivatives. + + Parameters: + - x: state vector [x, y, theta] + - u: control input vector [v, omega] + + Returns: + - A: Jacobian of dynamics with respect to the state x + - B: Jacobian of dynamics with respect to the control input u + """ + x_pos, y_pos, theta = x + v, omega = u + + # Compute A (Jacobian with respect to state) + A = np.zeros((self.state_dimension(), self.state_dimension())) + A[0, 2] = -v * np.sin(theta) + A[1, 2] = v * np.cos(theta) + + # Compute B (Jacobian with respect to control) + B = np.zeros((self.state_dimension(), self.control_dimension())) + B[0, 0] = np.cos(theta) + B[1, 0] = np.sin(theta) + B[2, 1] = 1.0 + + return A, B \ No newline at end of file diff --git a/guided_mrmp/utils/helpers.py b/guided_mrmp/utils/helpers.py index 1ab54d6023f43ede97d90a6d4dfe64574bfb8750..577736f1bc0bc9d6d4e5352c2607f2a178683906 100644 --- a/guided_mrmp/utils/helpers.py +++ b/guided_mrmp/utils/helpers.py @@ -19,22 +19,22 @@ def create_random_starts_and_goals(env, num_agents): for i in range(num_agents): # generate a random start - x = random.uniform(env.x_range[0], env.x_range[1]) - y = random.uniform(env.y_range[0], env.y_range[1]) + x = random.uniform(env.boundary[0][0], env.boundary[0][1]) + y = random.uniform(env.boundary[1][0], env.boundary[1][1]) while [x,y] in starts: - x = random.uniform(env.x_range[0], env.x_range[1]) - y = random.uniform(env.y_range[0], env.y_range[1]) + x = random.uniform(env.boundary[0][0], env.boundary[0][1]) + y = random.uniform(env.boundary[1][0], env.boundary[1][1]) - starts.append([x,y]) + starts.append([x,y,0]) # generate a random goal - x = random.uniform(env.x_range[0], env.x_range[1]) - y = random.uniform(env.y_range[0], env.y_range[1]) + x = random.uniform(env.boundary[0][0], env.boundary[0][1]) + y = random.uniform(env.boundary[1][0], env.boundary[1][1]) while [x,y] in goals: - x = random.uniform(env.x_range[0], env.x_range[1]) - y = random.uniform(env.y_range[0], env.y_range[1]) + x = random.uniform(env.boundary[0][0], env.boundary[0][1]) + y = random.uniform(env.boundary[1][0], env.boundary[1][1]) goals.append([x,y])