From cd5dc72fd153e35b17c7ef499eb73db0816d938c Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 13:18:39 -0700 Subject: [PATCH 01/17] Initial Trajectory Class added --- TrajectoryGeneration.py | 113 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 TrajectoryGeneration.py diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py new file mode 100644 index 0000000..ace97d8 --- /dev/null +++ b/TrajectoryGeneration.py @@ -0,0 +1,113 @@ +from control.matlab import * +import numpy as np +from math import cos, sin +from WooferConfig import WOOFER_CONFIG +from MathUtils import CrossProductMatrix +import cvxpy as cvx + +class TrajectoryGeneration(): + def __init__(self, dt): + """ + insert stuff + """ + self.dt = dt + self.J = WOOFER_CONFIG.INERTIA + self.m = WOOFER_CONFIG.MASS + + self.C = np.zeros((1,13)) + self.D = np.zeros((1,12)) + + + def rotZ(self, yaw): + Rz = np.zeros((3,3)) + + Rz[0,0] = cos(yaw) + Rz[0,1] = sin(yaw) + Rz[1,0] = -sin(yaw) + Rz[1,1] = cos(yaw) + Rz[2,2] = 1 + + return Rz + + def constructA(self, yaw): + """ + constructs the continuous time A matrix as a function of yaw + """ + A = np.zeros((13, 13)) + Rz = self.rotZ(yaw) + + A[0:3, 6:9] = eye(3) + A[3:6, 9:12] = Rz + A[8, 13] = 1 + + return A + + def constructB(self, foot_locations, yaw): + """ + constructs the continuous time B matrix as a function of yaw and foot locations + """ + B = np.zeros((13, 12)) + Rz = self.rotZ(yaw) + + J_w_inv = np.linalg.inv(Rz*self.J*Rz.T) + R1 = CrossProductMatrix(foot_locations[0:3]) + R2 = CrossProductMatrix(foot_locations[3:6]) + R3 = CrossProductMatrix(foot_locations[6:9]) + R4 = CrossProductMatrix(foot_locations[9:12]) + + B[6:9, 0:3] = eye(3)/self.m + B[6:9, 3:6] = eye(3)/self.m + B[6:9, 6:9] = eye(3)/self.m + B[6:9, 9:12] = eye(3)/self.m + + B[9:12, 0:3] = J_w_inv*R1 + B[9:12, 3:6] = J_w_inv*R2 + B[9:12, 6:9] = J_w_inv*R3 + B[9:12, 9:12] = J_w_inv*R4 + + return B + + def getDiscrete(self, A, B): + """ + get ZOH approximation of A and B matrices + """ + ct_sys = control.StateSpace(A, B, self.C, self.D) + dt_sys = control.matlab.c2d(ct_sys, self.dt) + + A_d = np.asarray(dt_sys.A) + B_d = np.asarray(dt_sys.B) + + sys = {"A_d": A_d, "B_d": B_d} + return sys + + def build_qp(x_ref, A_hat, B_hats, Qs, Rs, constr, c_low, c_up): + # n number of state variables + # k number of horizon state points + # m dimension of control input at each point + n, k = x_ref.shape + m = B_hats.shape[1] + + D = np.zeros((k * n, n)) + for i in range(k): + D[i * n: (i + 1) * n, :] = np.linalg.matrix_power(A_hat, i) + + C = np.zeros(k * n, m * k) + for i in range(k): + for j in range(i): + C[i * n: (i + 1) * n, j * m: (j + 1) * m] = \ + np.linalg.matrix_power(A_hat, (i - 1)) @ B_hats[j] + + Q = np.zeros(n * k, n * k) + R = np.zeros(m * k, m * k) + for i in range(k): + Q[i * n: (i + 1) * n, i * n: (i + 1) * n] = Qs[i] + M[i * m: (i + 1) * m, i * m: (i + 1) * m] = Rs[i] + + u = cvx.Variable(m * k) + target = x_ref.flatten() - D @ x_ref[:, 0] + cost = cvx.quad_form(C * u - target, Q) + cvx.quad_form(u, R) + obj = cvx.Minimize(cost) + constraints = [constr * u <= c_up, constr * u >= c_low] + prob = cvx.Problem(obj, constraints) + + return prob From ec43d39acef4f316948a7b8a89fd1a66bdd85d07 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 20:07:17 -0700 Subject: [PATCH 02/17] Basic MPC initial commit --- MPCPlanner.py | 47 +++++++++++++ SimulateWoofer.py | 12 ++-- TrajectoryGeneration.py | 152 ++++++++++++++++++++++++++++++++-------- TrotGait.py | 30 ++++++++ WooferRobot.py | 113 ++++++++++------------------- trajectoryTest.py | 29 ++++++++ 6 files changed, 274 insertions(+), 109 deletions(-) create mode 100644 MPCPlanner.py create mode 100644 TrotGait.py create mode 100644 trajectoryTest.py diff --git a/MPCPlanner.py b/MPCPlanner.py new file mode 100644 index 0000000..d78d03f --- /dev/null +++ b/MPCPlanner.py @@ -0,0 +1,47 @@ +import numpy as np +from WooferDynamics import * +from TrajectoryGeneration import TrajectoryGeneration +import rotations + +class MPCPlanner: + def __init__(self, N, dt_plan): + self.N = N + self.dt = dt_plan + +class MPCStandingPlanner(MPCPlanner): + def __init__(self, N, dt_plan, gait, init_state): + self.dt = dt_plan + self.N = N + self.gait = gait + self.trajectoryGenerator = TrajectoryGeneration(gait, dt_plan, N) + self.init_state = init_state + + def update(self, state, qpos_joints, foot_locations, t): + referenceTrajectory = self.generateReferenceTrajectory(state, foot_locations) + + U_horizon = self.trajectoryGenerator.solveSystem(referenceTrajectory, foot_locations, t) + + feet_forces = U_horizon[:12] + + joint_torques = np.zeros(12) + + rotmat = rotations.euler2mat(state[3:6]) + + for f in range(4): + # Extract the foot force vector for foot i + foot_force_world = feet_forces[f*3 : f*3 + 3] + + # Transform from world to body frames, + # The negative sign makes it the force exerted by the body + foot_force_body = -np.dot(rotmat.T, foot_force_world) + (beta,theta,r) = tuple(qpos_joints[f*3 : f*3 + 3]) + + # Transform from body frame forces into joint torques + joint_torques[f*3 : f*3 + 3] = np.dot(LegJacobian(beta, theta, r).T, foot_force_body) + + print("Joint torques: ", joint_torques) + + return joint_torques + + def generateReferenceTrajectory(self, state, foot_locations): + return np.tile(self.init_state[np.newaxis, :], (self.N, 1)) diff --git a/SimulateWoofer.py b/SimulateWoofer.py index 58220c4..b5d0cec 100644 --- a/SimulateWoofer.py +++ b/SimulateWoofer.py @@ -7,7 +7,7 @@ """ Initailize MuJoCo -""" +""" WooferXMLParser.Parse() model = load_model_from_path("woofer_out.xml") sim = MjSim(model) @@ -28,7 +28,7 @@ # Latency options latency = WOOFER_CONFIG.LATENCY # ms of latency between torque computation and application at the joint control_rate = WOOFER_CONFIG.UPDATE_PERIOD # ms between updates (not in Hz) -tau_history = np.zeros((12,timesteps)) +tau_history = np.zeros((12,timesteps)) tau_noise = WOOFER_CONFIG.JOINT_NOISE # Nm for i in range(timesteps): @@ -36,9 +36,9 @@ tau = woofer.step(sim) tau_history[:,i] = tau - if i%50 == 0: - # pass - woofer.print_data() + # if i%50 == 0: + # # pass + # woofer.print_data() # Run the control law according to the control rate if i%control_rate == 0: @@ -54,4 +54,4 @@ Save data to file """ woofer.save_logs() -print("Done Saving Logs") \ No newline at end of file +print("Done Saving Logs") diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index ace97d8..c239627 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -1,22 +1,39 @@ -from control.matlab import * +from scipy.signal import cont2discrete import numpy as np from math import cos, sin from WooferConfig import WOOFER_CONFIG from MathUtils import CrossProductMatrix import cvxpy as cvx +import pdb -class TrajectoryGeneration(): - def __init__(self, dt): +class TrajectoryGeneration: + def __init__(self, gait, dt, N): """ insert stuff """ - self.dt = dt self.J = WOOFER_CONFIG.INERTIA - self.m = WOOFER_CONFIG.MASS + self.m = WOOFER_CONFIG.MASS # 7.172 self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) + self.gait = gait + + # planning horizon + self.N = N + + # discretization step of planning horizon + self.dt = dt + + self.Q = np.eye(13) + self.alpha = 0 + self.R = self.alpha*np.eye(12) + + self.mu = 1.5 + + self.max_horz_force = 133 + self.max_vert_force = 133 + self.min_vert_force = 1 def rotZ(self, yaw): Rz = np.zeros((3,3)) @@ -36,9 +53,9 @@ def constructA(self, yaw): A = np.zeros((13, 13)) Rz = self.rotZ(yaw) - A[0:3, 6:9] = eye(3) + A[0:3, 6:9] = np.eye(3) A[3:6, 9:12] = Rz - A[8, 13] = 1 + A[8, 12] = 1 return A @@ -55,10 +72,10 @@ def constructB(self, foot_locations, yaw): R3 = CrossProductMatrix(foot_locations[6:9]) R4 = CrossProductMatrix(foot_locations[9:12]) - B[6:9, 0:3] = eye(3)/self.m - B[6:9, 3:6] = eye(3)/self.m - B[6:9, 6:9] = eye(3)/self.m - B[6:9, 9:12] = eye(3)/self.m + B[6:9, 0:3] = np.eye(3)/self.m + B[6:9, 3:6] = np.eye(3)/self.m + B[6:9, 6:9] = np.eye(3)/self.m + B[6:9, 9:12] = np.eye(3)/self.m B[9:12, 0:3] = J_w_inv*R1 B[9:12, 3:6] = J_w_inv*R2 @@ -71,43 +88,122 @@ def getDiscrete(self, A, B): """ get ZOH approximation of A and B matrices """ - ct_sys = control.StateSpace(A, B, self.C, self.D) - dt_sys = control.matlab.c2d(ct_sys, self.dt) + (A_d, B_d, _, _, _) = cont2discrete((A, B, self.C, self.D), self.dt) + + return A_d, B_d + + def solveSystem(self, refTrajectory, foot_locs, t): + """ + build matrices and call solver + + refTrajectory (of length N): + [pos, euler orientation, velocity, angular velocity] + """ + assert(refTrajectory.shape[0] == self.N) + yaw_bar = np.mean(refTrajectory[:, 3]) + + A = self.constructA(yaw_bar) + A_hat = None + B_hats = np.zeros((self.N, 13, 12)) + + curr_t = t + + for i in range(self.N): + active_feet = self.gait.feetInContact(curr_t) + active_feet_12 = active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] + + foot_loc_i = active_feet_12 * foot_locs + + B_i = self.constructB(foot_loc_i, refTrajectory[i, 3]) + + A_d, B_d = self.getDiscrete(A, B_i) + if i == 0: A_hat = A_d + B_hats[i] = B_d + + curr_t += self.dt + + C, c_up = self.makeConstraints(t) + prob, u = self.build_qp(refTrajectory, A_hat, B_hats, C, c_up) + + prob.solve(solver=cvx.OSQP) + return u.value + + + def makeConstraints(self, t): + """ + create constraint matrix C and lower and upper bounds + """ + curr_t = t + + C = np.zeros((self.N, 24, 12)) + c_up = np.zeros((self.N, 24)) + + fz_mat = np.zeros((4, 12)) + fx_mat = np.zeros((8, 12)) + fy_mat = np.zeros((8, 12)) + for i in range(4): + fz_mat[i, 3*i + 2] = 1 + + fx_mat[2*i, 3*i] = 1 + fx_mat[2*i, 3*i + 2] = -self.mu + fx_mat[2*i + 1, 3*i] = -1 + fx_mat[2*i + 1, 3*i + 2] = -self.mu + + fy_mat[2*i, 3*i + 1] = 1 + fy_mat[2*i, 3*i + 2] = -self.mu + fy_mat[2*i + 1, 3*i + 1] = -1 + fy_mat[2*i + 1, 3*i + 2] = -self.mu + + for i in range(self.N): + foot_loc_i = self.gait.feetInContact(curr_t) + # Z Force constraints, accounting for which feet are grounded + c_up[i, :4] = foot_loc_i * self.max_vert_force + C[i, :4] = fz_mat + c_up[i, 4:8] = -foot_loc_i * self.min_vert_force + C[i, 4:8] = -fz_mat + # X Force constraints, accounting for which feet are grounded + C[i, 8:16] = fx_mat + # Y Force constraints, accounting for which feet are grounded + C[i, 16:24] = fy_mat + + curr_t += self.dt - A_d = np.asarray(dt_sys.A) - B_d = np.asarray(dt_sys.B) + return C, c_up - sys = {"A_d": A_d, "B_d": B_d} - return sys - def build_qp(x_ref, A_hat, B_hats, Qs, Rs, constr, c_low, c_up): + def build_qp(self, x_ref, A_hat, B_hats, constr, c_up): # n number of state variables # k number of horizon state points # m dimension of control input at each point + x_ref = x_ref.T + x_ref = np.concatenate((x_ref, -9.81*np.ones((1, self.N)))) n, k = x_ref.shape - m = B_hats.shape[1] + k = self.N + m = B_hats.shape[2] D = np.zeros((k * n, n)) for i in range(k): D[i * n: (i + 1) * n, :] = np.linalg.matrix_power(A_hat, i) - C = np.zeros(k * n, m * k) + C = np.zeros((k * n, m * k)) for i in range(k): for j in range(i): C[i * n: (i + 1) * n, j * m: (j + 1) * m] = \ np.linalg.matrix_power(A_hat, (i - 1)) @ B_hats[j] - Q = np.zeros(n * k, n * k) - R = np.zeros(m * k, m * k) + Qs = np.zeros((n * k, n * k)) + Rs = np.zeros((m * k, m * k)) for i in range(k): - Q[i * n: (i + 1) * n, i * n: (i + 1) * n] = Qs[i] - M[i * m: (i + 1) * m, i * m: (i + 1) * m] = Rs[i] + Qs[i * n: (i + 1) * n, i * n: (i + 1) * n] = self.Q + Rs[i * m: (i + 1) * m, i * m: (i + 1) * m] = self.R u = cvx.Variable(m * k) - target = x_ref.flatten() - D @ x_ref[:, 0] - cost = cvx.quad_form(C * u - target, Q) + cvx.quad_form(u, R) + target = x_ref.T.flatten() - D @ x_ref[:, 0] + cost = cvx.quad_form(C * u - target, Qs) + cvx.quad_form(u, Rs) obj = cvx.Minimize(cost) - constraints = [constr * u <= c_up, constr * u >= c_low] + constraints = [] + for i in range(self.N): + constraints += [constr[i] * u[m*i:m*(i+1)] <= c_up[i]] prob = cvx.Problem(obj, constraints) - return prob + return prob, u diff --git a/TrotGait.py b/TrotGait.py new file mode 100644 index 0000000..6c1e9b0 --- /dev/null +++ b/TrotGait.py @@ -0,0 +1,30 @@ +import numpy as np + +class TrotGait: + def __init__(self, step_time, overlap_time): + """ + Four Phases: + Phase 1: FR/BL on ground + Phase 2: All feet on ground + Phase 3: FL/BR on ground + Phase 4: All feet on ground + repeat + Length of Phase 1/3: step_time + Length of Phase 2/4: overlap_time + """ + self.step_time = step_time + self.overlap_time = overlap_time + + self.phase_length = 2*step_time + 2*overlap_time + + def feetInContact(self, t): + phase_time = t % self.phase_length + + if phase_time < self.step_time: + return np.array([1, 0, 0, 1]) + elif phase_time < (self.step_time + self.overlap_time): + return np.ones(4) + elif phase_time < (2*self.step_time + self.overlap_time): + return np.array([0, 1, 1, 0]) + else: + return np.ones(4) diff --git a/WooferRobot.py b/WooferRobot.py index 58f2731..d46f6c9 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -7,7 +7,7 @@ from MathUtils import CrossProductMatrix, RunningMax # Provides kinematic functions among others -import WooferDynamics +import WooferDynamics from JointSpaceController import JointSpaceController, TrotPDController from BasicController import PropController @@ -16,15 +16,17 @@ from ContactEstimator import MuJoCoContactEstimator from GaitPlanner import StandingPlanner, StepPlanner from SwingLegController import PDSwingLegController, ZeroSwingLegController +from TrotGait import TrotGait +from MPCPlanner import MPCStandingPlanner from WooferConfig import WOOFER_CONFIG, QP_CONFIG, SWING_CONTROLLER_CONFIG, GAIT_PLANNER_CONFIG class WooferRobot(): """ - This class represents the onboard Woofer software. + This class represents the onboard Woofer software. The primary input is the mujoco simulation data and the - primary output is a set joint torques. + primary output is a set joint torques. """ def __init__(self, state_estimator, contact_estimator, qp_controller, gait_planner, swing_controller, dt): """ @@ -47,16 +49,13 @@ def __init__(self, state_estimator, contact_estimator, qp_controller, gait_plann self.data = {} self.data['torque_history'] = np.zeros((12,init_data_size)) self.data['force_history'] = np.zeros((12,init_data_size)) - self.data['ref_wrench_history'] = np.zeros((6,init_data_size)) self.data['contacts_history'] = np.zeros((4,init_data_size)) - self.data['active_feet_history'] = np.zeros((4,init_data_size)) + self.data['active_feet_history'] = np.zeros((4,init_data_size)) self.data['swing_torque_history'] = np.zeros((12,init_data_size)) self.data['swing_force_history'] = np.zeros((12,init_data_size)) self.data['swing_trajectory'] = np.zeros((12,init_data_size)) self.data['foot_positions'] = np.zeros((12,init_data_size)) - self.data['phase_history'] = np.zeros((1,init_data_size)) - self.data['step_phase_history'] = np.zeros((1,init_data_size)) self.dt = dt self.t = 0 @@ -64,24 +63,22 @@ def __init__(self, state_estimator, contact_estimator, qp_controller, gait_plann self.foot_forces = np.array([0,0,WOOFER_CONFIG.MASS*9.81/4]*4) - self.phase = 0 - self.step_phase = 0 # Increases from 0 to 1 and back to 0 every step + self.torques = np.zeros(12) + self.step_locations = np.zeros(12) - self.p_step_locations = np.zeros(12) self.swing_torques = np.zeros(12) self.swing_trajectory = np.zeros(12) - self.foot_positions = np.zeros(12) def step(self, sim): """ Get sensor data and update state estimate and contact estimate. Then calculate joint torques for locomotion. - + Details: Gait controller: Looks at phase variable to determine foot placements and COM trajectory - - QP: + + QP: Generates joint torques to achieve given desired CoM trajectory given stance feet Swing controller: @@ -91,60 +88,37 @@ def step(self, sim): self.state = self.state_estimator.update(sim) self.contacts = self.contact_estimator.update(sim) - ################################### Gait planning ################################### - (self.step_locations, self.p_step_locations, \ - p_ref, rpy_ref, self.active_feet, self.phase, self.step_phase) = self.gait_planner.update( self.state, - self.contacts, - self.t, - WOOFER_CONFIG, - GAIT_PLANNER_CONFIG) - # print("phase: %s"%self.phase) - - ################################### Swing leg control ################################### - # TODO. Zero for now, but in the future the swing controller will provide these torques - self.swing_torques, \ - self.swing_forces,\ - self.swing_trajectory, \ - self.foot_positions = self.swing_controller.update( self.state, - self.step_phase, - self.step_locations, - self.p_step_locations, - self.active_feet, - WOOFER_CONFIG, - SWING_CONTROLLER_CONFIG) - - ################################### QP force control ################################### - # Rearrange the state for the qp solver - qp_state = (self.state['p'], - self.state['p_d'], - self.state['q'], - self.state['w'], - self.state['j']) + # ################################### Swing leg control ################################### + # # TODO. Zero for now, but in the future the swing controller will provide these torques + # self.swing_torques, \ + # self.swing_forces,\ + # self.swing_trajectory, \ + # self.foot_positions = self.swing_controller.update( self.state, + # self.step_phase, + # self.step_locations, + # self.p_step_locations, + # self.active_feet, + # WOOFER_CONFIG, + # SWING_CONTROLLER_CONFIG) + + state = np.zeros(12) + state[0:3] = self.state['p'] + state[3:6] = rotations.quat2euler(self.state['q']) + state[6:9] = self.state['p_d'] + state[9:12] = self.state['w'] # Use forward kinematics from the robot body to compute where the woofer feet are self.feet_locations = WooferDynamics.LegForwardKinematics(self.state['q'], self.state['j']) - # Calculate foot forces using the QP solver - (qp_torques, self.foot_forces, self.ref_wrench) = self.qp_controller.Update(qp_state, - self.feet_locations, - self.active_feet, - p_ref, - rpy_ref, - self.foot_forces, - WOOFER_CONFIG, - QP_CONFIG) - # Expanded version of active feet - active_feet_12 = self.active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] - - # Mix the QP-generated torques and PD-generated torques to produce the final joint torques sent to the robot - self.torques = active_feet_12 * qp_torques + (1 - active_feet_12) * self.swing_torques + if(self.i % 50 == 0): + self.torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) # Update our record of the maximum force/torque - self.max_forces.Update(self.foot_forces) - self.max_torques.Update(self.torques) + # self.max_forces.Update(self.foot_forces) + # self.max_torques.Update(self.torques) # Log stuff - self.log_data() + # self.log_data() # Step time forward self.t += self.dt @@ -155,26 +129,23 @@ def step(self, sim): def log_data(self): """ Append data to logs - """ + """ data_len = self.data['torque_history'].shape[1] if self.i > data_len - 1: for key in self.data.keys(): self.data[key] = np.append(self.data[key], np.zeros((np.shape(self.data[key])[0],1000)),axis=1) - + self.max_forces.Update(self.foot_forces) self.max_torques.Update(self.torques) self.data['torque_history'][:,self.i] = self.torques self.data['force_history'][:,self.i] = self.foot_forces - self.data['ref_wrench_history'][:,self.i] = self.ref_wrench self.data['contacts_history'][:,self.i] = self.contacts self.data['active_feet_history'][:,self.i] = self.active_feet self.data['swing_torque_history'][:,self.i] = self.swing_torques self.data['swing_force_history'][:,self.i] = self.swing_forces self.data['swing_trajectory'][:,self.i] = self.swing_trajectory self.data['foot_positions'][:,self.i] = self.foot_positions - self.data['phase_history'][:,self.i] = self.phase - self.data['step_phase_history'][:,self.i] = self.step_phase def print_data(self): """ @@ -185,7 +156,6 @@ def print_data(self): print("Euler angles: %s" %rotations.quat2euler(self.state['q'])) print("Max gen. torques: %s"%self.max_torques.CurrentMax()) print("Max forces: %s" %self.max_forces.CurrentMax()) - print("Reference wrench: %s"%self.ref_wrench) print("feet locations: %s" %self.feet_locations) print("contacts: %s" %self.contacts) print("QP feet forces: %s" %self.foot_forces) @@ -207,17 +177,10 @@ def MakeWoofer(dt = 0.001): mujoco_state_est = MuJoCoStateEstimator() mujoco_contact_est = MuJoCoContactEstimator() qp_controller = QPBalanceController() - # gait_planner = StandingPlanner() - gait_planner = StepPlanner() + gait = TrotGait(0.5, 0.1) + gait_planner = MPCStandingPlanner(20, .05, gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0])) swing_controller = PDSwingLegController() woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, qp_controller, gait_planner, swing_controller, dt = dt) return woofer - - - - - - - diff --git a/trajectoryTest.py b/trajectoryTest.py new file mode 100644 index 0000000..6a77566 --- /dev/null +++ b/trajectoryTest.py @@ -0,0 +1,29 @@ +import numpy as np +from TrajectoryGeneration import TrajectoryGeneration +from TrotGait import TrotGait +from WooferConfig import WOOFER_CONFIG + + +N = 20 +discretization_step = 0.05 + +reference_trajectory = np.zeros((N, 12)) +reference_trajectory[:,0] = np.linspace(0, 1, N) +reference_trajectory[:,2] = 0.32*np.ones(N) + + +gait = TrotGait(0.5, 0.1) + +mpc = TrajectoryGeneration(gait, discretization_step, N) + + +foot_locs = np.zeros(12) +foot_locs[0:3]= np.array([WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.LEG_FB, 0]) +foot_locs[3:6] = np.array([-WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.LEG_FB, 0]) +foot_locs[6:9] = np.array([WOOFER_CONFIG.LEG_LR, -WOOFER_CONFIG.LEG_FB, 0]) +foot_locs[9:12] = np.array([-WOOFER_CONFIG.LEG_LR, -WOOFER_CONFIG.LEG_FB, 0]) + + +u = mpc.solveSystem(reference_trajectory, foot_locs, 0.0) + +print(u[0:12]) From 71aa22bd372691c304e261191d3d3e0fecb3d9f6 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 20:13:02 -0700 Subject: [PATCH 03/17] Remove unnecessary files --- GaitPlanner.py | 126 ----------------------------- PlotData.py | 39 --------- QPBalanceController.py | 88 -------------------- WooferQP.py | 177 ----------------------------------------- qp_test.py | 125 ----------------------------- woofer_test.py | 21 ----- 6 files changed, 576 deletions(-) delete mode 100644 GaitPlanner.py delete mode 100644 PlotData.py delete mode 100644 QPBalanceController.py delete mode 100644 WooferQP.py delete mode 100644 qp_test.py delete mode 100644 woofer_test.py diff --git a/GaitPlanner.py b/GaitPlanner.py deleted file mode 100644 index c033eb4..0000000 --- a/GaitPlanner.py +++ /dev/null @@ -1,126 +0,0 @@ -import numpy as np -from math import pi, cos, sin - -""" -Note: -Order of legs is: FR, FL, BR, BL -""" - -class GaitPlanner: - """ - Takes desired velocity and current state and outputs foot placements, CoM trajectory, and gait phase. - """ - def update(self, state, contacts, t): - """ - Output CoM trajectory and foot placements - """ - raise NotImplementedError - return (None, None, None, None, None, None) # Foot placements, CoM ref position, body ref orientation (euler), active_feet, phase - -class StandingPlanner(GaitPlanner): - """ - Stands still! - """ - def update(self, state, contacts, t): - freq = 1.0 - phase = t * 2 * pi * freq - p_ref = np.array([ sin(phase)*0.00, - cos(phase)*0.00, - sin(phase)*0.00 + 0.32]) - rpy_ref = np.array([ sin(phase)*15*pi/180.0, - cos(phase)*25*pi/180.0 + 0*pi/180, - sin(phase)*0*pi/180.0]) - - # Boolean representation of which feet the QP controller treats as in contact with the ground. - # A 1 in the ith slot means that the ith leg is in contact with the ground - active_feet = np.array([1,1,1,1]) - - return (None, None, p_ref, rpy_ref, active_feet, phase) - -class StepPlanner(GaitPlanner): - """ - Plans two walking-trot steps forward. During the first half of the stride, - the front-left and back-right legs are planned to be in stance. During the - second half of the stride, the front-right and back-left legs are planned - to be in stance. After the full stride, all legs are planned for stance. - """ - - def update(self, state, contacts, t, woof_config, gait_config): - """ - STEP_LENGTH: step length in meters - D: duration of the total two-step move in seconds - - phase: 0->0.5 & step_phase 0->1: moving FR and BL forward - phase: 0.5 -> 1 & step_phase 0->1: moving FL and BR forward - - Note that the foot placements vector is only used as reference positions for the swing controller - which means that the reference foot placements are meaningless for legs in contact - """ - - # stride starts at phase = 0 and ends at phase = 1 - phase = t/gait_config.D - - foot_height = woof_config.FOOT_RADIUS - - foot_locs = np.array([ woof_config.LEG_FB, -woof_config.LEG_LR, foot_height, - woof_config.LEG_FB, woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB, -woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB, woof_config.LEG_LR, foot_height]) - p_foot_locs = foot_locs - - active_feet = np.array([1,1,1,1]) - - step_phase = 0 - - if phase >= 0 and phase < 0.5: - # Move FR and BL forward - foot_locs = np.array([ woof_config.LEG_FB + gait_config.STEP_LENGTH, -woof_config.LEG_LR, foot_height, - woof_config.LEG_FB, woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB, -woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB + gait_config.STEP_LENGTH, woof_config.LEG_LR, foot_height]) - - p_foot_locs = np.array([ woof_config.LEG_FB, -woof_config.LEG_LR, foot_height, - woof_config.LEG_FB, woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB, -woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB, woof_config.LEG_LR, foot_height]) - active_feet = np.array([0,1,1,0]) - - step_phase = phase * 2.0 - - elif phase >= 0.5 and phase < 1.0: - # Move FL and BR forward - foot_locs = np.array([ woof_config.LEG_FB + gait_config.STEP_LENGTH, -woof_config.LEG_LR, foot_height, - woof_config.LEG_FB + gait_config.STEP_LENGTH, woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB + gait_config.STEP_LENGTH, -woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB + gait_config.STEP_LENGTH, woof_config.LEG_LR, foot_height]) - p_foot_locs = np.array([ woof_config.LEG_FB + gait_config.STEP_LENGTH, -woof_config.LEG_LR, foot_height, - woof_config.LEG_FB, woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB, -woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB + gait_config.STEP_LENGTH, woof_config.LEG_LR, foot_height]) - active_feet = np.array([1,0,0,1]) - - step_phase = (phase - 0.5) * 2.0 - - elif phase >= 1.0: - # All feet are forward - foot_locs = np.array([ woof_config.LEG_FB + gait_config.STEP_LENGTH, -woof_config.LEG_LR, foot_height, - woof_config.LEG_FB + gait_config.STEP_LENGTH, woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB + gait_config.STEP_LENGTH, -woof_config.LEG_LR, foot_height, - -woof_config.LEG_FB + gait_config.STEP_LENGTH, woof_config.LEG_LR, foot_height]) - p_foot_locs = foot_locs - - active_feet = np.array([1,1,1,1]) - - step_phase = 0.0 - - # Want body to be level during the step - rpy_ref = np.array([0,0,0]) - - # Want the body to move forward one step length - CoM_x = gait_config.STEP_LENGTH * np.clip(phase,0,1) - p_ref = np.array([CoM_x, 0, woof_config.LEG_L]) - - # print("foot placements:") - # print(foot_locs) - - return (foot_locs, p_foot_locs, p_ref, rpy_ref, active_feet, phase, step_phase) diff --git a/PlotData.py b/PlotData.py deleted file mode 100644 index 94f5df8..0000000 --- a/PlotData.py +++ /dev/null @@ -1,39 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import pickle - -### Init graphics ### -infile = open('woofer_logs.pickle','rb') -data = pickle.load(infile) -infile.close() - - -fig, (ax1, ax2,ax3,ax4,ax5) = plt.subplots(nrows=5, sharex=True) -# fig, (ax1, ax2,ax3,ax4,ax5,ax6) = plt.subplots(nrows=6, sharex=True) - -ax1.plot(data['torque_history'].T, linewidth=0.5) -ax1.set_title('torque_history') - -ax2.plot(data['force_history'].T,linewidth=0.5) -ax2.set_title('force_history') - -ref = ax3.plot(data['ref_wrench_history'].T,linewidth=0.5) -ax3.set_title('ref_wrench_history') -ax3.legend(ref, ('x','y','z','r','p','y')) - -ax4.plot(np.array([1,1.05,1.1,1.15])*data['contacts_history'].T,linewidth=0.5) -ax4.set_title('contacts_history') - -ax5.plot(np.array([1,1.05,1.1,1.15])*data['active_feet_history'].T,linewidth=0.5) -ax5.set_title('active_feet_history') - -# ax6.plot(smooth_contacts_history.T,linewidth=0.5) - - -fig2, (ax1,ax2,ax3,ax4) = plt.subplots(nrows = 4,sharex = True) -ax1.plot(data['swing_force_history'].T, linewidth=0.5) -ax2.plot(data['swing_trajectory'].T, linewidth=0.5) -ax3.plot(data['phase_history'].T, linewidth=0.5) -ax4.plot(data['step_phase_history'].T, linewidth=0.5) - -plt.show() \ No newline at end of file diff --git a/QPBalanceController.py b/QPBalanceController.py deleted file mode 100644 index 3623ba0..0000000 --- a/QPBalanceController.py +++ /dev/null @@ -1,88 +0,0 @@ -from BasicController import PropController -import rotations -from WooferDynamics import * -from WooferQP import SolveFeetForces - -class QPBalanceController: - def __init__(self): - self.max_forces = 0 - self.max_gen_torques = 0 - - def Update(self,coordinates, - feet_locations, - active_feet, - o_ref, - rpy_ref, - f_prev, - woof_config, - qp_config, - verbose = 0): - """ - Run the QP Balance controller - """ - - (xyz, v_xyz, orientation, w_rpy, qpos_joints) = coordinates - - ########## Generate desired body accelerations ########## - rpy = rotations.quat2euler(orientation) - rotmat = rotations.quat2mat(orientation) - - # Foot Kinematics - feet_locations = LegForwardKinematics(orientation, qpos_joints) - - # Feet contact - contacts = active_feet - - # Use a whole-body PD controller to generate desired body moments - ### Cartesian force ### - wn_cart = 20 # desired natural frequency - zeta_cart = 0.8 # desired damping ratio - kp_cart = wn_cart**2 # wn^2 - kd_cart = 2*wn_cart*zeta_cart # 2wn*zeta - a_xyz = PropController( xyz, o_ref, kp_cart) + \ - PropController( v_xyz, 0, kd_cart) - a_xyz += np.array([0,0,9.81]) - f_xyz = woof_config.MASS * a_xyz - - ### Angular moment ### - wn_ang = 20 - zeta_ang = 0.8 - kp_ang = wn_ang**2 - kd_ang = 2*wn_ang*zeta_ang - a_rpy = PropController( rpy, rpy_ref, kp_ang) + \ - PropController( w_rpy, 0.0, kd_ang) - tau_rpy = np.matmul(woof_config.INERTIA, a_rpy) - - ### Combined force and moment ### - ref_wrench = np.concatenate([f_xyz, tau_rpy]) - - - ########## Solve for foot forces ######### - - # Find foot forces to achieve the desired body moments - feet_forces = SolveFeetForces( feet_locations, - contacts, - ref_wrench, - f_prev, - mu = qp_config.MU, - alpha = qp_config.ALPHA, - beta = qp_config.BETA, - gamma = qp_config.GAMMA, - verbose = verbose) - - joint_torques = np.zeros(12) - - for f in range(4): - # Extract the foot force vector for foot i - foot_force_world = feet_forces[f*3 : f*3 + 3] - - # Transform from world to body frames, - # The negative sign makes it the force exerted by the body - foot_force_body = -np.dot(rotmat.T, foot_force_world) - (beta,theta,r) = tuple(qpos_joints[f*3 : f*3 + 3]) - - # Transform from body frame forces into joint torques - joint_torques[f*3 : f*3 + 3] = np.dot(LegJacobian(beta, theta, r).T, foot_force_body) - - - return (joint_torques, feet_forces, ref_wrench) \ No newline at end of file diff --git a/WooferQP.py b/WooferQP.py deleted file mode 100644 index 94731d9..0000000 --- a/WooferQP.py +++ /dev/null @@ -1,177 +0,0 @@ -import osqp -import scipy.sparse as sparse -import numpy as np -import math - -from MathUtils import CrossProductMatrix - -def AccelerationMatrix(q_feet): - """ - Generates the matrix that maps from foot forces to body accelerations in x,y,z and pitch,roll,yaw - From Equation (5) in the paper "High-slope terrain locomotion for torque-controlled quadruped robots" - - q_feet: 12 vector of the foot locations, in world coordinates centered around the robot COM - - Note: The 12-vector of foot forces are ground reaction forces, so for example, if the foot is pushing down, - the z-component is >= 0 - """ - A = np.zeros((6,12)) - A[0:3,0:3] = np.eye(3) - A[0:3,3:6] = np.eye(3) - A[0:3,6:9] = np.eye(3) - A[0:3,9:12] = np.eye(3) - A[3:6,0:3] = CrossProductMatrix(q_feet[0:3]) - A[3:6,3:6] = CrossProductMatrix(q_feet[3:6]) - A[3:6,6:9] = CrossProductMatrix(q_feet[6:9]) - A[3:6,9:12] = CrossProductMatrix(q_feet[9:12]) - return A - - -def SolveFeetForces(q_feet, feet_contact, reference_wrench, f_prev, mu = 1.0, alpha = 1e-3, beta = 0.1, gamma = 100, verbose=0): - """ - Use OSQP to solve for feet forces. - - q_feet : Location of feet relative to body COM in world coordinates - (yaw may be in local coordinates) - reference_wrench : reference force and torque on the body - mu : Friction coefficient - alpha : Weight on normalizer for feet forces - beta : Weight on smoothness between feet forces - gamma : Scaling factor on angular acceleration deviations - - OSQP uses an objective of the form: - (1/2)x'Px + qx - and linear ineq constraints: - lb < Cx < ub - - Our objective is to select foot forces that minimize the difference between the - realized body acceleration and a desired body acceleration. - - This is akin to: - Minimize ||Ax-b||^2 - However, we want to prioritize angular stability of the body, so we use a quadratic norm of K: - Minimize [(Ax-b)'K(Ax-b)] - - - When expanded, it becomes: - Minimize x'A'KAx - 2A'Kbx + b'Kb - We can write in the OSPQ form with the following substitutions - P = 2A'KA - q = -2A'Kb - - We also want to minimize the norm of x to minimize foot forces, and minimize the - difference between new and old foot forces so the final objective is - - Minimize x'(A'KA + R + beta*I)x - 2(A'Kb + beta*x0) - P = 2(A'KA + R + betaI) - q = - 2A'Kb - beta*x0 - - The derivation of the smoothness term is - loss = beta*(x1-x0)'(x1-x0) = beta*x1'x1 - beta*2*x0'x1 + beta*x0'x0 - - in the code A'KA is called AKA, R is alpha*I, and x0 is called f_prev (previous force) - """ - - ## Params ## - max_horz_force = 133 - max_vert_force = 133 - min_vert_force = 1 - - A = AccelerationMatrix(q_feet) - b = reference_wrench - - # Construct normalizer matrix R which minimizes total foot forces - R = np.eye(12)*alpha - - # Construct quadratic norm matrix K - K = np.diag([1,1,1,gamma,gamma,gamma]) - - # Construct AKA - AKA = np.matmul(A.T,np.matmul(K, A)) - - # Construct the final P matrix - P_dense = 2*(AKA + R + beta*np.eye(12)) - P = sparse.csc_matrix(P_dense) - - # Construct the linear term q - q = -2*np.matmul(A.T,np.matmul(K, b)) - beta*f_prev - - # Set up the inequality constraints with matrix X - mu_pyramid = mu/(2**0.5) - # cone: fx^2+fy^2 <= mu^2 * fz^2 - C = np.zeros((28,12)) - - # Enforce friction pyramid constraints on all 4 feet - for i in range(4): - # fz >= 0 - C[i*5, i*3+2] = 1 - # ufz+fx >= 0 - C[i*5+1, i*3] = 1 - C[i*5+1, i*3+2] = mu - # ufz-fx >= 0 - C[i*5+2, i*3] = -1 - C[i*5+2, i*3+2] = mu - # ufz+fy >= 0 - C[i*5+3, i*3+1] = 1 - C[i*5+3, i*3+2] = mu - # ufz-fy >= 0 - C[i*5+4, i*3+1] = -1 - C[i*5+4, i*3+2] = mu - - # Enforce limits on horizontal components - for i in range(4): - C[i*2+20, i*3] = 1 - C[i*2+20+1, i*3+1]= 1 - - C = sparse.csc_matrix(C) - lb = np.zeros((28,)) - ub = np.array([np.inf]*28) - for i in range(4): - lb[i*5] = min_vert_force if feet_contact[i] else 0 - ub[i*5] = max_vert_force if feet_contact[i] else 0 - - # Enforce that the horizontal ||foot forces|| are <= 40 - lb[20:] = -max_horz_force - ub[20:] = max_horz_force - - # print(feet_contact, ub) - - prob = osqp.OSQP() - prob.setup(P,q,C,lb,ub,alpha=1.0,verbose=0) - res = prob.solve() - - if verbose>0: - print('------------ QP Outputs ------------') - print('Desired acceleration') - print(b) - print('Foot forces:') - print(res.x) - print('Status: %s'%res.info.status) - print('Actual accleration') - print(np.dot(A,res.x)) - - - acc_cost = np.matmul(res.x, np.matmul(AKA, res.x)) + np.dot(q,res.x) + np.dot(b,b) - force_cost = np.dot(res.x, np.dot(R, res.x)) - print('Accuracy cost: %s \t Force cost: %s'%(acc_cost, force_cost)) - print('\n') - - return res.x - - -# # generate basic foot config -# # this is for testing only -# np.set_printoptions(suppress=True) -# body_w = 0.4 #[m] -# body_l = 0.8 #[m] -# stance_h = 0.6 #[m] -# xfl = np.array([-body_w/2, body_l/2, -stance_h]) -# xfr = np.array([body_w/2, body_l/2, -stance_h]) -# xbl = np.array([-body_w/2, -body_l/2, -stance_h]) -# xbr = np.array([body_w/2, -body_l/2, -stance_h]) - -# q_feet = np.concatenate((xfl,xfr,xbl,xbr)) -# # generate acceleration matrix - -# reference_wrench = np.array([0, 0, 9.81, 1, 0,0 ]) -# SolveFeetForces(q_feet, reference_wrench) diff --git a/qp_test.py b/qp_test.py deleted file mode 100644 index f096539..0000000 --- a/qp_test.py +++ /dev/null @@ -1,125 +0,0 @@ -import cvxpy as cp -import numpy as np - -np.set_printoptions(suppress=True) - -l1 = 0.2 #[m] - -body_w = 0.4 #[m] -body_l = 0.8 #[m] -stance_h = 0.6 #[m] - -body_m = 8.0 #[kg] -n = 4 #[num feet] - -def CrossProductMatrix(a): - return np.array([[0, -a[2], a[1]], \ - [a[2], 0, -a[0]], \ - [-a[1], a[0], 0]]) - -def AccelerationMatrix(q_feet): - """ - Generates the matrix that maps from foot forces to body accelerations in x,y,z and pitch,roll,yaw - From Equation (5) in the paper "High-slope terrain locomotion for torque-controlled quadruped robots" - - q_feet: 12-vector containing the foot reaction forces, ie if the foot is pushing down, - the z-component is >= 0 - """ - A = np.zeros((6,12)) - A[0:3,0:3] = np.eye(3) - A[0:3,3:6] = np.eye(3) - A[0:3,6:9] = np.eye(3) - A[0:3,9:12] = np.eye(3) - A[3:6,0:3] = CrossProductMatrix(q_feet[0:3]) - A[3:6,3:6] = CrossProductMatrix(q_feet[3:6]) - A[3:6,6:9] = CrossProductMatrix(q_feet[6:9]) - A[3:6,9:12] = CrossProductMatrix(q_feet[9:12]) - return A - -# generate basic foot config -# this is for testing only -xfl = np.array([-body_w/2, body_l/2, -stance_h]) -xfr = np.array([body_w/2, body_l/2, -stance_h]) -xbl = np.array([-body_w/2, -body_l/2, -stance_h]) -xbr = np.array([body_w/2, -body_l/2, -stance_h]) - -q_feet = np.concatenate((xfl,xfr,xbl,xbr)) -# generate acceleration matrix -A = AccelerationMatrix(q_feet) - -# verify A -print('Map between foot force and acceleration:') -print(A) - -##################### Four-foot Test (Stand) ######################### - -# target body acceleration -b = np.zeros((6,1)) -b[2] = 10 # [m/s2] -b[3] = 1 # [rad/s2] - -##### Construct the problem ####### -x = cp.Variable(3*n) -objective = cp.Minimize(cp.sum_squares(A*x - b) + (1e-3)*cp.norm(x)) - -# Constrain vertical component of foot forces to be >= 0 -constraints = [0 <= x[2], 0 <= x[5], 0 <= x[8], 0 <= x[11]] -prob = cp.Problem(objective, constraints) - -# Solve for foot forces -result = prob.solve() - -print('------------Four-foot test (stand)------------') -print('Desired acceleration') -print(b) -print('Foot forces:') -print(x.value) -print(prob.status) -print('Actual accleration') -print(np.dot(A,x.value)) - -##################### Two-foot Test (Trot) ######################### -# Find foot forces when you set the condition that only two legs are in contact with the ground -b = np.zeros((6,1)) -b[2] = 10 # [m/s2] -b[3] = 1 # [rad/s2] -b[4] = -1 # [rad/s2] -x = cp.Variable(3*n) -objective = cp.Minimize(cp.sum_squares(A*x - b) + (1e-3)*cp.norm(x)) -constraints = [0 <= x[2], 0 <= x[5], 0 <= x[8], 0 <= x[11]] -constraints = constraints + [0 <= x[3:6], 0 >= x[3:6], 0 <= x[6:9], 0 >= x[6:9]] -prob = cp.Problem(objective, constraints) -result = prob.solve() -print('------------Two-foot test (trot)------------') -print('Desired acceleration') -print(b) -print('Foot forces:') -print(x.value) -print(prob.status) -print('Actual accleration') -print(np.dot(A,x.value)) - -##################### Three-foot Test (Walk) ######################### -# Find foot forces when you set the condition that only two legs are in contact with the ground -b = np.zeros((6,1)) -b[2] = 10 # [m/s2] -b[3] = 1 # [rad/s2] -b[4] = -1 # [rad/s2] -x = cp.Variable(3*n) -objective = cp.Minimize(cp.sum_squares(A*x - b) + (1e-3)*cp.norm(x)) -constraints = [0 <= x[2], 0 <= x[5], 0 <= x[8], 0 <= x[11]] -constraints = constraints + [0 <= x[0:3], 0 >= x[0:3]] -prob = cp.Problem(objective, constraints) -result = prob.solve() -print('------------Three-foot test (trot)------------') -print('Desired acceleration') -print(b) -print('Foot forces:') -print(x.value) -print(prob.status) -print('Actual accleration') -print(np.dot(A,x.value)) - - - - diff --git a/woofer_test.py b/woofer_test.py deleted file mode 100644 index 87f1307..0000000 --- a/woofer_test.py +++ /dev/null @@ -1,21 +0,0 @@ -from mujoco_py import load_model_from_path, MjSim, MjViewer -import numpy as np -np.set_printoptions(suppress=True) - -import woofer_xml_parser - -model = load_model_from_path("woofer_out.xml") -print(model) -sim = MjSim(model) -viewer = MjViewer(sim) - -sim.step() - -print("Nicely exposed function:\n") -print(sim.model.get_xml()) -# viewer.render() - - - - - From 4e51bba9a9b29ab1f4ba5f69c60d00345c60ab1d Mon Sep 17 00:00:00 2001 From: Milind Date: Sat, 1 Jun 2019 20:41:19 -0700 Subject: [PATCH 04/17] remove imports --- WooferRobot.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/WooferRobot.py b/WooferRobot.py index d46f6c9..a119c68 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -11,10 +11,8 @@ from JointSpaceController import JointSpaceController, TrotPDController from BasicController import PropController -from QPBalanceController import QPBalanceController from StateEstimator import MuJoCoStateEstimator from ContactEstimator import MuJoCoContactEstimator -from GaitPlanner import StandingPlanner, StepPlanner from SwingLegController import PDSwingLegController, ZeroSwingLegController from TrotGait import TrotGait from MPCPlanner import MPCStandingPlanner @@ -28,14 +26,13 @@ class WooferRobot(): The primary input is the mujoco simulation data and the primary output is a set joint torques. """ - def __init__(self, state_estimator, contact_estimator, qp_controller, gait_planner, swing_controller, dt): + def __init__(self, state_estimator, contact_estimator, gait_planner, swing_controller, dt): """ Initialize object variables """ self.contact_estimator = contact_estimator self.state_estimator = state_estimator - self.qp_controller = qp_controller # QP controller for calculating foot forces self.gait_planner = gait_planner self.swing_controller = swing_controller self.state = None @@ -176,11 +173,10 @@ def MakeWoofer(dt = 0.001): """ mujoco_state_est = MuJoCoStateEstimator() mujoco_contact_est = MuJoCoContactEstimator() - qp_controller = QPBalanceController() gait = TrotGait(0.5, 0.1) gait_planner = MPCStandingPlanner(20, .05, gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0])) swing_controller = PDSwingLegController() - woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, qp_controller, gait_planner, swing_controller, dt = dt) + woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, gait_planner, swing_controller, dt = dt) return woofer From d59714db447a9063e6c36220c422f10137080abd Mon Sep 17 00:00:00 2001 From: Milind Date: Sat, 1 Jun 2019 22:11:55 -0700 Subject: [PATCH 05/17] Better ref trajectory for standing still --- MPCPlanner.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/MPCPlanner.py b/MPCPlanner.py index d78d03f..52168ab 100644 --- a/MPCPlanner.py +++ b/MPCPlanner.py @@ -22,7 +22,10 @@ def update(self, state, qpos_joints, foot_locations, t): U_horizon = self.trajectoryGenerator.solveSystem(referenceTrajectory, foot_locations, t) feet_forces = U_horizon[:12] - + #ground = feet_forces > 0.1 + #feet_forces = np.zeros(12) + #feet_forces[ground] = 7.172*9.81/ground.sum() + joint_torques = np.zeros(12) rotmat = rotations.euler2mat(state[3:6]) @@ -39,9 +42,14 @@ def update(self, state, qpos_joints, foot_locations, t): # Transform from body frame forces into joint torques joint_torques[f*3 : f*3 + 3] = np.dot(LegJacobian(beta, theta, r).T, foot_force_body) - print("Joint torques: ", joint_torques) + print("Feet forces z: ", feet_forces[2::3]) return joint_torques def generateReferenceTrajectory(self, state, foot_locations): - return np.tile(self.init_state[np.newaxis, :], (self.N, 1)) + scale = np.linspace(0, 1, self.N)[:, np.newaxis] + curr = np.tile(state[np.newaxis, :], (self.N, 1)) + goal = np.tile(self.init_state[np.newaxis, :], (self.N, 1)) + ref = scale * goal + (1 - scale) * curr + ref[:, 6:] = (goal[:, 6:] - curr[:, 6:]) / (self.dt * self.N) + return ref From bfcc178e058553c3a097d3d6628a3b1160819492 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 22:13:25 -0700 Subject: [PATCH 06/17] Added walking heuristic and improved gait class --- SwingLegController.py | 16 +++++----- TrotGait.py | 71 +++++++++++++++++++++++++++++++++++-------- WooferRobot.py | 37 ++++++++++++++-------- 3 files changed, 91 insertions(+), 33 deletions(-) diff --git a/SwingLegController.py b/SwingLegController.py index 6c45eb4..d0a47f5 100644 --- a/SwingLegController.py +++ b/SwingLegController.py @@ -1,6 +1,7 @@ import numpy as np import WooferDynamics from math import pi, sin +from WooferConfig import WOOFER_CONFIG, SWING_CONTROLLER_CONFIG class SwingLegController: def update(self, state, contacts, step_phase): @@ -18,7 +19,8 @@ class PDSwingLegController(SwingLegController): Computes joint torques to bring the swing legs to their new locations """ - def trajectory(self, state, step_phase, step_locs, p_step_locs, active_feet, swing_config): + + def trajectory(self, state, step_phase, step_locs, p_step_locs, active_feet): """ sin2-based parametric trajectory planner @@ -33,7 +35,7 @@ def trajectory(self, state, step_phase, step_locs, p_step_locs, active_feet, swi # foot heights foot_heights = np.zeros(12) - foot_heights[[2,5,8,11]] = swing_config.STEP_HEIGHT * (sin(step_phase * pi))**2 + foot_heights[[2,5,8,11]] = SWING_CONTROLLER_CONFIG.STEP_HEIGHT * (sin(step_phase * pi))**2 # combine ground plane interp and foot heights swing_foot_reference_p = WooferDynamics.FootSelector(1-active_feet)*(ground_plane_foot_reference + foot_heights) @@ -44,13 +46,13 @@ def trajectory(self, state, step_phase, step_locs, p_step_locs, active_feet, swi return swing_foot_reference_p - def update(self, state, step_phase, step_locs, p_step_locs, active_feet, woof_config, swing_config): - reference_positions = self.trajectory(state, step_phase, step_locs, p_step_locs, active_feet, swing_config) + def update(self, state, step_phase, step_locs, p_step_locs, active_feet): + reference_positions = self.trajectory(state, step_phase, step_locs, p_step_locs, active_feet) actual_positions = WooferDynamics.FootLocationsWorld(state) errors = reference_positions - actual_positions - foot_forces = WooferDynamics.CoordinateExpander(swing_config.KP) * errors * WooferDynamics.FootSelector(1-active_feet) + foot_forces = WooferDynamics.CoordinateExpander(SWING_CONTROLLER_CONFIG.KP) * errors * WooferDynamics.FootSelector(1-active_feet) leg_torques = np.zeros(12) for i in range(4): @@ -62,7 +64,3 @@ def update(self, state, step_phase, step_locs, p_step_locs, active_feet, woof_co # print(leg_torques, reference_positions) # print(" ") return leg_torques, foot_forces, reference_positions, actual_positions - - - - diff --git a/TrotGait.py b/TrotGait.py index 6c1e9b0..f6ea715 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -4,27 +4,74 @@ class TrotGait: def __init__(self, step_time, overlap_time): """ Four Phases: - Phase 1: FR/BL on ground - Phase 2: All feet on ground - Phase 3: FL/BR on ground - Phase 4: All feet on ground + Phase 1: All feet on ground + Phase 2: FR/BL on ground + Phase 3: All feet on ground + Phase 4: FL/BR on ground repeat Length of Phase 1/3: step_time Length of Phase 2/4: overlap_time """ - self.step_time = step_time - self.overlap_time = overlap_time + self.step_time = overlap_time + self.overlap_time = step_time self.phase_length = 2*step_time + 2*overlap_time - def feetInContact(self, t): + def getPhase(self, t): phase_time = t % self.phase_length - if phase_time < self.step_time: - return np.array([1, 0, 0, 1]) + if phase_time < self.overlap_time: + return 1 elif phase_time < (self.step_time + self.overlap_time): - return np.ones(4) - elif phase_time < (2*self.step_time + self.overlap_time): - return np.array([0, 1, 1, 0]) + return 2 + elif phase_time < (self.step_time + 2*self.overlap_time): + return 3 else: + return 4 + + def feetInContact(self, t): + phase = self.getPhase(t) + + if phase == 1: return np.ones(4) + elif phase == 2: + return np.array([1, 0, 0, 1]) + elif phase == 3: + return np.array([1, 1, 1, 1]) + else: + return np.array([0, 1, 1, 0]) + + def updateStepLocations(self, state, step_locations, p_step_locations, phase): + """ + uses the heuristic in eq. 33 of the MIT Cheetah 3 MPC paper + to calculate the footstep locations + + side: right leg = 1, left leg = 0 + """ + + p_diff = 0.5 * state['p_d'][0:2] * self.step_time + + new_step_locations = step_locations + new_p_step_locations = p_step_locations + + if phase == 1: + # need to move the FL/BR feet + new_step_locations[3:5] = step_locations[3:5] + p_diff + new_step_locations[6:8] = step_locations[6:8] + p_diff + + elif phase == 2: + # moving FL/BR feet + new_p_step_locations[3:5] = p_step_locations[3:5] + new_p_step_locations[6:8] = p_step_locations[6:8] + + elif phase == 3: + # need to move the FR/BL feet + new_step_locations[0:2] = step_locations[0:2] + p_diff + new_step_locations[9:11] = step_locations[9:11] + p_diff + + elif phase == 4: + # moving FR/BL feet + new_p_step_locations[0:2] = p_step_locations[0:2] + p_diff + new_p_step_locations[9:11] = p_step_locations[9:11] + p_diff + + return (new_step_locations, new_p_step_locations) diff --git a/WooferRobot.py b/WooferRobot.py index a119c68..48f8d50 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -58,11 +58,20 @@ def __init__(self, state_estimator, contact_estimator, gait_planner, swing_contr self.t = 0 self.i = 0 + self.phase = 1 + self.foot_forces = np.array([0,0,WOOFER_CONFIG.MASS*9.81/4]*4) self.torques = np.zeros(12) - self.step_locations = np.zeros(12) + self.step_locations = np.array([WOOFER_CONFIG.LEG_FB, -WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS, + WOOFER_CONFIG.LEG_FB, WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS, + -WOOFER_CONFIG.LEG_FB, -WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS, + -WOOFER_CONFIG.LEG_FB, WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS]) + self.p_step_locations = np.array([WOOFER_CONFIG.LEG_FB, -WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS, + WOOFER_CONFIG.LEG_FB, WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS, + -WOOFER_CONFIG.LEG_FB, -WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS, + -WOOFER_CONFIG.LEG_FB, WOOFER_CONFIG.LEG_LR, WOOFER_CONFIG.FOOT_RADIUS]) self.swing_torques = np.zeros(12) self.swing_trajectory = np.zeros(12) @@ -85,18 +94,22 @@ def step(self, sim): self.state = self.state_estimator.update(sim) self.contacts = self.contact_estimator.update(sim) + self.phase = self.gait.getPhase(self.t) + + (self.step_locations, self.p_step_locations) = self.gait.updateStepLocations(self.state, + self.step_locations, + self.p_step_locations, + self.phase) + # ################################### Swing leg control ################################### - # # TODO. Zero for now, but in the future the swing controller will provide these torques - # self.swing_torques, \ - # self.swing_forces,\ - # self.swing_trajectory, \ - # self.foot_positions = self.swing_controller.update( self.state, - # self.step_phase, - # self.step_locations, - # self.p_step_locations, - # self.active_feet, - # WOOFER_CONFIG, - # SWING_CONTROLLER_CONFIG) + self.swing_torques, \ + self.swing_forces,\ + self.swing_trajectory, \ + self.foot_positions = self.swing_controller.update( self.state, + self.step_phase, + self.step_locations, + self.p_step_locations, + self.active_feet) state = np.zeros(12) state[0:3] = self.state['p'] From cca2e08a4356b0dfd9ba415e5465b8e720a02451 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 22:22:02 -0700 Subject: [PATCH 07/17] Fixed typos --- TrotGait.py | 13 ++++++------- WooferRobot.py | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/TrotGait.py b/TrotGait.py index f6ea715..b81229c 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -29,7 +29,7 @@ def getPhase(self, t): else: return 4 - def feetInContact(self, t): + def feetInContact(self, phase): phase = self.getPhase(t) if phase == 1: @@ -42,13 +42,12 @@ def feetInContact(self, t): return np.array([0, 1, 1, 0]) def updateStepLocations(self, state, step_locations, p_step_locations, phase): - """ - uses the heuristic in eq. 33 of the MIT Cheetah 3 MPC paper - to calculate the footstep locations - - side: right leg = 1, left leg = 0 - """ + """ + uses the heuristic in eq. 33 of the MIT Cheetah 3 MPC paper + to calculate the footstep locations + side: right leg = 1, left leg = 0 + """ p_diff = 0.5 * state['p_d'][0:2] * self.step_time new_step_locations = step_locations diff --git a/WooferRobot.py b/WooferRobot.py index 48f8d50..b0e9c3e 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -26,7 +26,7 @@ class WooferRobot(): The primary input is the mujoco simulation data and the primary output is a set joint torques. """ - def __init__(self, state_estimator, contact_estimator, gait_planner, swing_controller, dt): + def __init__(self, state_estimator, contact_estimator, gait, gait_planner, swing_controller, dt): """ Initialize object variables """ @@ -96,6 +96,8 @@ def step(self, sim): self.phase = self.gait.getPhase(self.t) + self.active_feet = self.gait.feetInContact(self.phase) + (self.step_locations, self.p_step_locations) = self.gait.updateStepLocations(self.state, self.step_locations, self.p_step_locations, @@ -120,8 +122,14 @@ def step(self, sim): # Use forward kinematics from the robot body to compute where the woofer feet are self.feet_locations = WooferDynamics.LegForwardKinematics(self.state['q'], self.state['j']) - if(self.i % 50 == 0): - self.torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) + # only + if(self.i % (self.gait_planner.dt/self.dt) == 0): + mpc_torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) + + # Expanded version of active feet + active_feet_12 = self.active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] + + self.torques = active_feet_12 * qp_torques + (1 - active_feet_12) * self.swing_torques # Update our record of the maximum force/torque # self.max_forces.Update(self.foot_forces) @@ -190,6 +198,6 @@ def MakeWoofer(dt = 0.001): gait_planner = MPCStandingPlanner(20, .05, gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0])) swing_controller = PDSwingLegController() - woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, gait_planner, swing_controller, dt = dt) + woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, gait, gait_planner, swing_controller, dt = dt) return woofer From 86f340a2c20163de813a883a6217ef73548e6764 Mon Sep 17 00:00:00 2001 From: Milind Date: Sat, 1 Jun 2019 22:27:36 -0700 Subject: [PATCH 08/17] Fixes --- TrajectoryGeneration.py | 4 ++-- TrotGait.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index c239627..e6b6aa8 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -109,7 +109,7 @@ def solveSystem(self, refTrajectory, foot_locs, t): curr_t = t for i in range(self.N): - active_feet = self.gait.feetInContact(curr_t) + active_feet = self.gait.feetInContact(self.gait.getPhase(curr_t)) active_feet_12 = active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] foot_loc_i = active_feet_12 * foot_locs @@ -155,7 +155,7 @@ def makeConstraints(self, t): fy_mat[2*i + 1, 3*i + 2] = -self.mu for i in range(self.N): - foot_loc_i = self.gait.feetInContact(curr_t) + foot_loc_i = self.gait.feetInContact(self.gait.getPhase(curr_t)) # Z Force constraints, accounting for which feet are grounded c_up[i, :4] = foot_loc_i * self.max_vert_force C[i, :4] = fz_mat diff --git a/TrotGait.py b/TrotGait.py index b81229c..a63aa06 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -30,8 +30,6 @@ def getPhase(self, t): return 4 def feetInContact(self, phase): - phase = self.getPhase(t) - if phase == 1: return np.ones(4) elif phase == 2: From d764f21e62df7067531fcbc4f739dd2eeda7ecfd Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 22:33:52 -0700 Subject: [PATCH 09/17] Added step phase --- TrotGait.py | 12 ++++++++++++ WooferRobot.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/TrotGait.py b/TrotGait.py index a63aa06..d3a9087 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -72,3 +72,15 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): new_p_step_locations[9:11] = p_step_locations[9:11] + p_diff return (new_step_locations, new_p_step_locations) + + def getStepPhase(self, t): + """ + returns step phase for swing leg controller + """ + phase_time = t % self.phase_length + + if phase == 4: + time_into_traj = 2*self.step_time + 2*self.overlap_time - phase_time + else: + time_into_traj = self.step_time + self.overlap_time - phase_time + return time_into_traj/self.step_time diff --git a/WooferRobot.py b/WooferRobot.py index b0e9c3e..88a9230 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -35,6 +35,7 @@ def __init__(self, state_estimator, contact_estimator, gait, gait_planner, swing self.state_estimator = state_estimator self.gait_planner = gait_planner self.swing_controller = swing_controller + self.gait = gait self.state = None self.contacts = None @@ -96,6 +97,8 @@ def step(self, sim): self.phase = self.gait.getPhase(self.t) + self.step_phase = self.gait.getStepPhase(self.t) + self.active_feet = self.gait.feetInContact(self.phase) (self.step_locations, self.p_step_locations) = self.gait.updateStepLocations(self.state, From 651ac9f1168ef3fbfff7bc500362dc497f26e1ba Mon Sep 17 00:00:00 2001 From: Tarun Date: Sat, 1 Jun 2019 22:44:30 -0700 Subject: [PATCH 10/17] Working Woofer Robot with swing leg torques --- BasicController.py | 2 +- TrajectoryGeneration.py | 2 +- TrotGait.py | 2 +- WooferConfig.py | 6 +++--- WooferRobot.py | 8 +++++--- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/BasicController.py b/BasicController.py index cca148a..5fdab30 100644 --- a/BasicController.py +++ b/BasicController.py @@ -6,4 +6,4 @@ def PropController(qpos,refpos,kp): Note: the damping for a PD controller should be added to the joint in the xml file to increase the stability of the simulation """ - return kp*(refpos-qpos) \ No newline at end of file + return kp*(refpos-qpos) diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index e6b6aa8..7bbc346 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -12,7 +12,7 @@ def __init__(self, gait, dt, N): insert stuff """ self.J = WOOFER_CONFIG.INERTIA - self.m = WOOFER_CONFIG.MASS # 7.172 + self.m = 7.172 # WOOFER_CONFIG.MASS self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) diff --git a/TrotGait.py b/TrotGait.py index d3a9087..2b66c56 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -73,7 +73,7 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): return (new_step_locations, new_p_step_locations) - def getStepPhase(self, t): + def getStepPhase(self, t, phase): """ returns step phase for swing leg controller """ diff --git a/WooferConfig.py b/WooferConfig.py index 1a09439..d8cab37 100644 --- a/WooferConfig.py +++ b/WooferConfig.py @@ -29,8 +29,8 @@ def __init__(self): self.INERTIA[2,2] = Iz self.JOINT_NOISE = 0.5 # Nm, 1 sigma of gaussian noise - self.LATENCY = 2 # ms of sense->control latency - self.UPDATE_PERIOD = 2 # ms between control updates + self.LATENCY = 0 # ms of sense->control latency + self.UPDATE_PERIOD = 1 # ms between control updates class EnvironmentConfig: def __init__(self): @@ -60,4 +60,4 @@ def __init__(self): QP_CONFIG = QPConfig() SWING_CONTROLLER_CONFIG = SwingControllerConfig() GAIT_PLANNER_CONFIG = GaitPlannerConfig() -ENVIRONMENT_CONFIG = EnvironmentConfig() \ No newline at end of file +ENVIRONMENT_CONFIG = EnvironmentConfig() diff --git a/WooferRobot.py b/WooferRobot.py index 88a9230..2e59941 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -76,6 +76,7 @@ def __init__(self, state_estimator, contact_estimator, gait, gait_planner, swing self.swing_torques = np.zeros(12) self.swing_trajectory = np.zeros(12) + self.mpc_torques = np.zeros(12) def step(self, sim): """ @@ -97,7 +98,7 @@ def step(self, sim): self.phase = self.gait.getPhase(self.t) - self.step_phase = self.gait.getStepPhase(self.t) + self.step_phase = self.gait.getStepPhase(self.t, self.phase) self.active_feet = self.gait.feetInContact(self.phase) @@ -126,13 +127,14 @@ def step(self, sim): self.feet_locations = WooferDynamics.LegForwardKinematics(self.state['q'], self.state['j']) # only + if(self.i % (self.gait_planner.dt/self.dt) == 0): - mpc_torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) + self.mpc_torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) # Expanded version of active feet active_feet_12 = self.active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] - self.torques = active_feet_12 * qp_torques + (1 - active_feet_12) * self.swing_torques + self.torques = active_feet_12 * self.mpc_torques + (1 - active_feet_12) * self.swing_torques # Update our record of the maximum force/torque # self.max_forces.Update(self.foot_forces) From 12c4171ee751ab000d509a2d10040939150dcacc Mon Sep 17 00:00:00 2001 From: Tarun Date: Sun, 2 Jun 2019 13:57:08 -0700 Subject: [PATCH 11/17] Walking structure implemented --- MPCPlanner.py | 24 +++++++++++++++-------- SwingLegController.py | 2 +- TrotGait.py | 44 +++++++++++++++++++++++++------------------ WooferConfig.py | 2 +- WooferRobot.py | 26 ++++++++++++++++--------- 5 files changed, 61 insertions(+), 37 deletions(-) diff --git a/MPCPlanner.py b/MPCPlanner.py index 52168ab..ddb066f 100644 --- a/MPCPlanner.py +++ b/MPCPlanner.py @@ -8,24 +8,26 @@ def __init__(self, N, dt_plan): self.N = N self.dt = dt_plan -class MPCStandingPlanner(MPCPlanner): - def __init__(self, N, dt_plan, gait, init_state): +class MPCWalkingPlanner(MPCPlanner): + def __init__(self, N, dt_plan, gait, init_state, desired_velocity): self.dt = dt_plan self.N = N self.gait = gait self.trajectoryGenerator = TrajectoryGeneration(gait, dt_plan, N) self.init_state = init_state + self.desired_velocity = desired_velocity def update(self, state, qpos_joints, foot_locations, t): - referenceTrajectory = self.generateReferenceTrajectory(state, foot_locations) + referenceTrajectory = self.generateReferenceTrajectory(state) U_horizon = self.trajectoryGenerator.solveSystem(referenceTrajectory, foot_locations, t) feet_forces = U_horizon[:12] + feet_forces[np.abs(feet_forces) < 1e-5] = 0 #ground = feet_forces > 0.1 #feet_forces = np.zeros(12) #feet_forces[ground] = 7.172*9.81/ground.sum() - + joint_torques = np.zeros(12) rotmat = rotations.euler2mat(state[3:6]) @@ -42,14 +44,20 @@ def update(self, state, qpos_joints, foot_locations, t): # Transform from body frame forces into joint torques joint_torques[f*3 : f*3 + 3] = np.dot(LegJacobian(beta, theta, r).T, foot_force_body) - print("Feet forces z: ", feet_forces[2::3]) + # print("Feet forces: ", feet_forces) + # print("Angular velocity: ", state[9:12]) return joint_torques - def generateReferenceTrajectory(self, state, foot_locations): + def generateReferenceTrajectory(self, state): scale = np.linspace(0, 1, self.N)[:, np.newaxis] curr = np.tile(state[np.newaxis, :], (self.N, 1)) - goal = np.tile(self.init_state[np.newaxis, :], (self.N, 1)) + goal = np.zeros((self.N, 12)) + goal[:, 6:9] = self.desired_velocity ref = scale * goal + (1 - scale) * curr - ref[:, 6:] = (goal[:, 6:] - curr[:, 6:]) / (self.dt * self.N) + ref_vel = ref[:, 6:].copy() + ref_vel *= self.dt + dx = np.cumsum(ref_vel, axis=0) + ref[:, :3] = curr[:, :3] + dx[:, :3] + ref[:, 2:6] = (1 - scale) * curr[:, 2:6] + scale * self.init_state[2:6][np.newaxis, :] return ref diff --git a/SwingLegController.py b/SwingLegController.py index d0a47f5..0047df5 100644 --- a/SwingLegController.py +++ b/SwingLegController.py @@ -61,6 +61,6 @@ def update(self, state, step_phase, step_locs, p_step_locs, active_feet): specific_leg_joints = state['j'][3*i:3*i+3] leg_torques[3*i:3*i+3] = WooferDynamics.FootForceToJointTorques(specific_foot_force, specific_leg_joints, state['q'], abaduction_offset = 0) - # print(leg_torques, reference_positions) + # print("Reference world positions: ", reference_positions) # print(" ") return leg_torques, foot_forces, reference_positions, actual_positions diff --git a/TrotGait.py b/TrotGait.py index 2b66c56..f2d3f57 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -1,4 +1,5 @@ import numpy as np +from WooferConfig import WOOFER_CONFIG class TrotGait: def __init__(self, step_time, overlap_time): @@ -12,8 +13,8 @@ def __init__(self, step_time, overlap_time): Length of Phase 1/3: step_time Length of Phase 2/4: overlap_time """ - self.step_time = overlap_time - self.overlap_time = step_time + self.step_time = step_time + self.overlap_time = overlap_time self.phase_length = 2*step_time + 2*overlap_time @@ -46,41 +47,48 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): side: right leg = 1, left leg = 0 """ - p_diff = 0.5 * state['p_d'][0:2] * self.step_time + # p_diff = 0.5 * state['p_d'][0:2] * self.step_time + + p_diff = np.array([0.1, 0.0]) new_step_locations = step_locations new_p_step_locations = p_step_locations if phase == 1: # need to move the FL/BR feet - new_step_locations[3:5] = step_locations[3:5] + p_diff - new_step_locations[6:8] = step_locations[6:8] + p_diff + new_step_locations[3:5] = p_step_locations[3:5] + p_diff + new_step_locations[6:8] = p_step_locations[6:8] + p_diff + new_p_step_locations[0:2] = p_step_locations[0:2] + new_p_step_locations[9:11] = p_step_locations[9:11] - elif phase == 2: - # moving FL/BR feet - new_p_step_locations[3:5] = p_step_locations[3:5] - new_p_step_locations[6:8] = p_step_locations[6:8] + # elif phase == 2: + # # moving FL/BR feet + # elif phase == 3: # need to move the FR/BL feet - new_step_locations[0:2] = step_locations[0:2] + p_diff - new_step_locations[9:11] = step_locations[9:11] + p_diff + new_step_locations[0:2] = p_step_locations[0:2] + p_diff + new_step_locations[9:11] = p_step_locations[9:11] + p_diff + new_p_step_locations[3:5] = p_step_locations[3:5] + new_p_step_locations[6:8] = p_step_locations[6:8] - elif phase == 4: - # moving FR/BL feet - new_p_step_locations[0:2] = p_step_locations[0:2] + p_diff - new_p_step_locations[9:11] = p_step_locations[9:11] + p_diff + # elif phase == 4: + # # moving FR/BL feet + # return (new_step_locations, new_p_step_locations) + # return (step_locations, p_step_locations) + def getStepPhase(self, t, phase): """ returns step phase for swing leg controller """ phase_time = t % self.phase_length + time_into_traj = 0 if phase == 4: - time_into_traj = 2*self.step_time + 2*self.overlap_time - phase_time - else: - time_into_traj = self.step_time + self.overlap_time - phase_time + time_into_traj = phase_time - (self.step_time + 2*self.overlap_time) + elif phase == 2: + time_into_traj = phase_time - self.overlap_time return time_into_traj/self.step_time diff --git a/WooferConfig.py b/WooferConfig.py index d8cab37..68ba025 100644 --- a/WooferConfig.py +++ b/WooferConfig.py @@ -28,7 +28,7 @@ def __init__(self): self.INERTIA[1,1] = Iy self.INERTIA[2,2] = Iz - self.JOINT_NOISE = 0.5 # Nm, 1 sigma of gaussian noise + self.JOINT_NOISE = 0#.5 # Nm, 1 sigma of gaussian noise self.LATENCY = 0 # ms of sense->control latency self.UPDATE_PERIOD = 1 # ms between control updates diff --git a/WooferRobot.py b/WooferRobot.py index 2e59941..1a209a1 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -15,7 +15,7 @@ from ContactEstimator import MuJoCoContactEstimator from SwingLegController import PDSwingLegController, ZeroSwingLegController from TrotGait import TrotGait -from MPCPlanner import MPCStandingPlanner +from MPCPlanner import MPCWalkingPlanner from WooferConfig import WOOFER_CONFIG, QP_CONFIG, SWING_CONTROLLER_CONFIG, GAIT_PLANNER_CONFIG @@ -96,6 +96,9 @@ def step(self, sim): self.state = self.state_estimator.update(sim) self.contacts = self.contact_estimator.update(sim) + # Use forward kinematics from the robot body to compute where the woofer feet are + self.feet_locations = WooferDynamics.LegForwardKinematics(self.state['q'], self.state['j']) + self.phase = self.gait.getPhase(self.t) self.step_phase = self.gait.getStepPhase(self.t, self.phase) @@ -106,7 +109,6 @@ def step(self, sim): self.step_locations, self.p_step_locations, self.phase) - # ################################### Swing leg control ################################### self.swing_torques, \ self.swing_forces,\ @@ -123,11 +125,7 @@ def step(self, sim): state[6:9] = self.state['p_d'] state[9:12] = self.state['w'] - # Use forward kinematics from the robot body to compute where the woofer feet are - self.feet_locations = WooferDynamics.LegForwardKinematics(self.state['q'], self.state['j']) - - # only - + # only update the mpc trajectory at rate of planning discretization if(self.i % (self.gait_planner.dt/self.dt) == 0): self.mpc_torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) @@ -197,10 +195,20 @@ def MakeWoofer(dt = 0.001): """ Create robot object """ + trot_overlap_time = .01 + trot_step_time = .1 + + MPC_horizon = 10 + MPC_planning_timestep = 0.01 + + MPC_desired_velocity = np.array([0.3, 0.0, 0.0]) + mujoco_state_est = MuJoCoStateEstimator() mujoco_contact_est = MuJoCoContactEstimator() - gait = TrotGait(0.5, 0.1) - gait_planner = MPCStandingPlanner(20, .05, gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0])) + gait = TrotGait(trot_step_time, trot_overlap_time) + gait_planner = MPCWalkingPlanner(MPC_horizon, MPC_planning_timestep, + gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0]), + MPC_desired_velocity) swing_controller = PDSwingLegController() woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, gait, gait_planner, swing_controller, dt = dt) From e6368af745fae38149443fb485aec1eb1ad628f6 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sun, 2 Jun 2019 14:38:33 -0700 Subject: [PATCH 12/17] Fixed bad B matrix --- TrajectoryGeneration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index 7bbc346..4fe7a4f 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -12,7 +12,7 @@ def __init__(self, gait, dt, N): insert stuff """ self.J = WOOFER_CONFIG.INERTIA - self.m = 7.172 # WOOFER_CONFIG.MASS + self.m = 7.172 # WOOFER_CONFIG.MASS self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) @@ -77,10 +77,10 @@ def constructB(self, foot_locations, yaw): B[6:9, 6:9] = np.eye(3)/self.m B[6:9, 9:12] = np.eye(3)/self.m - B[9:12, 0:3] = J_w_inv*R1 - B[9:12, 3:6] = J_w_inv*R2 - B[9:12, 6:9] = J_w_inv*R3 - B[9:12, 9:12] = J_w_inv*R4 + B[9:12, 0:3] = J_w_inv @ R1 + B[9:12, 3:6] = J_w_inv @ R2 + B[9:12, 6:9] = J_w_inv @ R3 + B[9:12, 9:12] = J_w_inv @ R4 return B From 877b388d492f168281c814bb64557209cad3492b Mon Sep 17 00:00:00 2001 From: Tarun Date: Sun, 2 Jun 2019 17:20:52 -0700 Subject: [PATCH 13/17] Fixing little things, closer to walking --- MPCPlanner.py | 71 ++++++++++++++++++++++++++++++++++++----- TrajectoryGeneration.py | 19 +++++++---- TrotGait.py | 41 +++++++++++++++++------- WooferConfig.py | 4 +-- WooferRobot.py | 16 +++++++--- 5 files changed, 120 insertions(+), 31 deletions(-) diff --git a/MPCPlanner.py b/MPCPlanner.py index ddb066f..8f6428c 100644 --- a/MPCPlanner.py +++ b/MPCPlanner.py @@ -18,7 +18,7 @@ def __init__(self, N, dt_plan, gait, init_state, desired_velocity): self.desired_velocity = desired_velocity def update(self, state, qpos_joints, foot_locations, t): - referenceTrajectory = self.generateReferenceTrajectory(state) + referenceTrajectory = self.generateReferenceTrajectory(state, t) U_horizon = self.trajectoryGenerator.solveSystem(referenceTrajectory, foot_locations, t) @@ -49,15 +49,70 @@ def update(self, state, qpos_joints, foot_locations, t): return joint_torques - def generateReferenceTrajectory(self, state): + def generateReferenceTrajectory(self, state, t): scale = np.linspace(0, 1, self.N)[:, np.newaxis] curr = np.tile(state[np.newaxis, :], (self.N, 1)) goal = np.zeros((self.N, 12)) - goal[:, 6:9] = self.desired_velocity - ref = scale * goal + (1 - scale) * curr - ref_vel = ref[:, 6:].copy() + goal[:, 6:8] = self.desired_velocity[:2] + + ref = np.zeros((self.N, 12)) + # Interpolate x and y velocities to the desired velocity + ref[:, 6:8] = (1 - scale) * curr[:, 6:8] + scale * goal[:, 6:8] + # Calculate reference x and y positions by integration + ref_vel = ref[:, 6:8].copy() + ref_vel *= self.dt + dxy = np.cumsum(ref_vel, axis=0) + ref[:, :2] = curr[:, :2] + dxy + # Interpolate angle positions to 0 + ref[:, 3:6] = (1 - scale) * curr[:, 3:6] + # Interpolate z position to initial state + ref[:, 2] = (1 - scale)[:, 0] * curr[:, 2] + scale[:, 0] * self.init_state[2] + # Set z, angle velocities to 0 + # They are already 0 + return ref + +class MPCOrientationPlanner(MPCWalkingPlanner): + def __init__(self, N, dt_plan, gait, init_state): + self.dt = dt_plan + self.N = N + self.gait = gait + self.trajectoryGenerator = TrajectoryGeneration(gait, dt_plan, N) + self.init_state = init_state + + def generateReferenceTrajectory(self, state, t): + ROLL_CONSTANT = 0.0 + PITCH_CONSTANT = 0.0 + YAW_CONSTANT = 0.025 + w = 2 * np.pi * 10 + + scale = np.linspace(0, 1, self.N)[:, np.newaxis] + curr = np.tile(state[np.newaxis, :], (self.N, 1)) + goal = np.zeros((self.N, 12)) + + ref = np.zeros((self.N, 12)) + # Interpolate x and y velocities to the desired velocity + ref[:, 6:8] = (1 - scale) * curr[:, 6:8] + scale * goal[:, 6:8] + # Calculate reference x and y positions by integration + ref_vel = ref[:, 6:8].copy() ref_vel *= self.dt - dx = np.cumsum(ref_vel, axis=0) - ref[:, :3] = curr[:, :3] + dx[:, :3] - ref[:, 2:6] = (1 - scale) * curr[:, 2:6] + scale * self.init_state[2:6][np.newaxis, :] + dxy = np.cumsum(ref_vel, axis=0) + ref[:, :2] = curr[:, :2] + dxy + # Interpolate angle positions to 0 + ref[:, 3:6] = (1 - scale) * curr[:, 3:6] + # Interpolate z position to initial state + ref[:, 2] = (1 - scale)[:, 0] * curr[:, 2] + scale[:, 0] * self.init_state[2] + # Set z, angle velocities to 0 + # They are already 0 + + times = np.linspace(t, t + (self.N - 1) * self.dt, self.N) + ref[:, 3] = ROLL_CONSTANT * np.sin(w * times) + ref[:, 4] = PITCH_CONSTANT * np.sin(w * times) + ref[:, 5] = YAW_CONSTANT * np.sin(w * times) + ref[:, 9] = w * ROLL_CONSTANT * np.cos(w * times) + ref[:, 10] = w * PITCH_CONSTANT * np.cos(w * times) + ref[:, 11] = w * YAW_CONSTANT * np.cos(w * times) + + # if t > 2.0: + # import pdb; pdb.set_trace() + return ref diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index 4fe7a4f..016846f 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -12,7 +12,8 @@ def __init__(self, gait, dt, N): insert stuff """ self.J = WOOFER_CONFIG.INERTIA - self.m = 7.172 # WOOFER_CONFIG.MASS + self.m = WOOFER_CONFIG.MASS # 7.172 + # self.m = 7.172 self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) @@ -66,7 +67,7 @@ def constructB(self, foot_locations, yaw): B = np.zeros((13, 12)) Rz = self.rotZ(yaw) - J_w_inv = np.linalg.inv(Rz*self.J*Rz.T) + J_w_inv = np.linalg.inv(Rz @ self.J @ Rz.T) R1 = CrossProductMatrix(foot_locations[0:3]) R2 = CrossProductMatrix(foot_locations[3:6]) R3 = CrossProductMatrix(foot_locations[6:9]) @@ -92,7 +93,7 @@ def getDiscrete(self, A, B): return A_d, B_d - def solveSystem(self, refTrajectory, foot_locs, t): + def solveSystem(self, refTrajectory, foot_hist, t): """ build matrices and call solver @@ -102,6 +103,9 @@ def solveSystem(self, refTrajectory, foot_locs, t): assert(refTrajectory.shape[0] == self.N) yaw_bar = np.mean(refTrajectory[:, 3]) + # if t > 1.0: + # import pdb; pdb.set_trace() + A = self.constructA(yaw_bar) A_hat = None B_hats = np.zeros((self.N, 13, 12)) @@ -109,10 +113,10 @@ def solveSystem(self, refTrajectory, foot_locs, t): curr_t = t for i in range(self.N): - active_feet = self.gait.feetInContact(self.gait.getPhase(curr_t)) - active_feet_12 = active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] + # active_feet = self.gait.feetInContact(self.gait.getPhase(curr_t)) + # active_feet_12 = active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] - foot_loc_i = active_feet_12 * foot_locs + foot_loc_i = foot_hist[i,:] B_i = self.constructB(foot_loc_i, refTrajectory[i, 3]) @@ -122,6 +126,8 @@ def solveSystem(self, refTrajectory, foot_locs, t): curr_t += self.dt + # if t > 0.1: + # import pdb; pdb.set_trace() C, c_up = self.makeConstraints(t) prob, u = self.build_qp(refTrajectory, A_hat, B_hats, C, c_up) @@ -175,6 +181,7 @@ def build_qp(self, x_ref, A_hat, B_hats, constr, c_up): # n number of state variables # k number of horizon state points # m dimension of control input at each point + x_ref = x_ref.T x_ref = np.concatenate((x_ref, -9.81*np.ones((1, self.N)))) n, k = x_ref.shape diff --git a/TrotGait.py b/TrotGait.py index f2d3f57..8542bdb 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -1,5 +1,6 @@ import numpy as np from WooferConfig import WOOFER_CONFIG +from WooferDynamics import FootSelector, CoordinateExpander class TrotGait: def __init__(self, step_time, overlap_time): @@ -45,11 +46,15 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): uses the heuristic in eq. 33 of the MIT Cheetah 3 MPC paper to calculate the footstep locations + todo: update step locations based off where they actually land + side: right leg = 1, left leg = 0 """ # p_diff = 0.5 * state['p_d'][0:2] * self.step_time - p_diff = np.array([0.1, 0.0]) + p_diff = np.array([0.2, 0.0]) + + # p_diff = np.zeros(2) new_step_locations = step_locations new_p_step_locations = p_step_locations @@ -61,10 +66,6 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): new_p_step_locations[0:2] = p_step_locations[0:2] new_p_step_locations[9:11] = p_step_locations[9:11] - # elif phase == 2: - # # moving FL/BR feet - # - elif phase == 3: # need to move the FR/BL feet new_step_locations[0:2] = p_step_locations[0:2] + p_diff @@ -72,14 +73,8 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): new_p_step_locations[3:5] = p_step_locations[3:5] new_p_step_locations[6:8] = p_step_locations[6:8] - # elif phase == 4: - # # moving FR/BL feet - # - return (new_step_locations, new_p_step_locations) - # return (step_locations, p_step_locations) - def getStepPhase(self, t, phase): """ returns step phase for swing leg controller @@ -92,3 +87,27 @@ def getStepPhase(self, t, phase): elif phase == 2: time_into_traj = phase_time - self.overlap_time return time_into_traj/self.step_time + + def constuctFutureFootHistory(self, t, state, current_foot_locations, step_locations, N, mpc_dt): + """ + + """ + foot_hist = np.zeros((N, 12)) + + future_step_locations = step_locations - CoordinateExpander(state['p']) + + t_i = t + phase_i = self.getPhase(t_i) + prev_phase = phase_i + foot_hist[0, :] = FootSelector(self.feetInContact(phase_i)) * current_foot_locations + + for i in range(1,N): + if(phase_i != prev_phase): + foot_hist[i, :] = FootSelector(self.feetInContact(phase_i)) * future_step_locations + else: + foot_hist[i, :] = foot_hist[i-1, :] + prev_phase = phase_i + t_i += mpc_dt + phase_i = self.getPhase(t_i) + + return foot_hist diff --git a/WooferConfig.py b/WooferConfig.py index 68ba025..b99352f 100644 --- a/WooferConfig.py +++ b/WooferConfig.py @@ -35,7 +35,7 @@ def __init__(self): class EnvironmentConfig: def __init__(self): self.MU = 1.5 # coeff friction - self.SIM_STEPS = 1500 # simulation steps to take + self.SIM_STEPS = 3500 # simulation steps to take self.DT = 0.001 # timestep [s] # Software stuff @@ -48,7 +48,7 @@ def __init__(self): class SwingControllerConfig: def __init__(self): - self.STEP_HEIGHT = 0.08 # [m] + self.STEP_HEIGHT = 0.1 # [m] self.KP = np.array([400,400,2000]) # [Nm/rad, Nm/rad, N/m] class GaitPlannerConfig: diff --git a/WooferRobot.py b/WooferRobot.py index 1a209a1..43df812 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -15,7 +15,7 @@ from ContactEstimator import MuJoCoContactEstimator from SwingLegController import PDSwingLegController, ZeroSwingLegController from TrotGait import TrotGait -from MPCPlanner import MPCWalkingPlanner +from MPCPlanner import MPCWalkingPlanner, MPCOrientationPlanner from WooferConfig import WOOFER_CONFIG, QP_CONFIG, SWING_CONTROLLER_CONFIG, GAIT_PLANNER_CONFIG @@ -127,7 +127,13 @@ def step(self, sim): # only update the mpc trajectory at rate of planning discretization if(self.i % (self.gait_planner.dt/self.dt) == 0): - self.mpc_torques = self.gait_planner.update(state, self.state['j'], self.feet_locations, self.t) + self.foot_hist = self.gait.constuctFutureFootHistory(self.t, + self.state, + self.feet_locations, + self.step_locations, + self.gait_planner.N, + self.gait_planner.dt) + self.mpc_torques = self.gait_planner.update(state, self.state['j'], self.foot_hist, self.t) # Expanded version of active feet active_feet_12 = self.active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] @@ -195,8 +201,8 @@ def MakeWoofer(dt = 0.001): """ Create robot object """ - trot_overlap_time = .01 - trot_step_time = .1 + trot_overlap_time = .05 + trot_step_time = .3 MPC_horizon = 10 MPC_planning_timestep = 0.01 @@ -209,6 +215,8 @@ def MakeWoofer(dt = 0.001): gait_planner = MPCWalkingPlanner(MPC_horizon, MPC_planning_timestep, gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0]), MPC_desired_velocity) + # gait_planner = MPCOrientationPlanner(MPC_horizon, MPC_planning_timestep, + # gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0])) swing_controller = PDSwingLegController() woofer = WooferRobot(mujoco_state_est, mujoco_contact_est, gait, gait_planner, swing_controller, dt = dt) From f375aaccdc93bbcc26a843d20f5c028b31f33030 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sun, 2 Jun 2019 19:34:33 -0700 Subject: [PATCH 14/17] Walking for a few steps --- TrajectoryGeneration.py | 4 ++-- TrotGait.py | 21 +++++++++++++++------ WooferConfig.py | 2 +- WooferRobot.py | 7 ++++--- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index 016846f..97408e9 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -12,8 +12,8 @@ def __init__(self, gait, dt, N): insert stuff """ self.J = WOOFER_CONFIG.INERTIA - self.m = WOOFER_CONFIG.MASS # 7.172 - # self.m = 7.172 + # self.m = WOOFER_CONFIG.MASS # 7.172 + self.m = 7.9 self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) diff --git a/TrotGait.py b/TrotGait.py index 8542bdb..2f742d8 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -17,6 +17,8 @@ def __init__(self, step_time, overlap_time): self.step_time = step_time self.overlap_time = overlap_time + self.isFirstStep = 1 + self.phase_length = 2*step_time + 2*overlap_time def getPhase(self, t): @@ -50,9 +52,16 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): side: right leg = 1, left leg = 0 """ - # p_diff = 0.5 * state['p_d'][0:2] * self.step_time + p_diff = 2.5 * state['p_d'][0:2] * self.step_time + + print(p_diff) - p_diff = np.array([0.2, 0.0]) + # if self.isFirstStep: + # p_diff = np.array([0.15, 0.0]) + # if phase == 2: + # self.isFirstStep = 0 + # else: + # p_diff = np.array([0.315, 0.0]) # p_diff = np.zeros(2) @@ -63,15 +72,15 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): # need to move the FL/BR feet new_step_locations[3:5] = p_step_locations[3:5] + p_diff new_step_locations[6:8] = p_step_locations[6:8] + p_diff - new_p_step_locations[0:2] = p_step_locations[0:2] - new_p_step_locations[9:11] = p_step_locations[9:11] + new_p_step_locations[0:2] = step_locations[0:2] + new_p_step_locations[9:11] = step_locations[9:11] elif phase == 3: # need to move the FR/BL feet new_step_locations[0:2] = p_step_locations[0:2] + p_diff new_step_locations[9:11] = p_step_locations[9:11] + p_diff - new_p_step_locations[3:5] = p_step_locations[3:5] - new_p_step_locations[6:8] = p_step_locations[6:8] + new_p_step_locations[3:5] = step_locations[3:5] + new_p_step_locations[6:8] = step_locations[6:8] return (new_step_locations, new_p_step_locations) diff --git a/WooferConfig.py b/WooferConfig.py index b99352f..84bbdfc 100644 --- a/WooferConfig.py +++ b/WooferConfig.py @@ -35,7 +35,7 @@ def __init__(self): class EnvironmentConfig: def __init__(self): self.MU = 1.5 # coeff friction - self.SIM_STEPS = 3500 # simulation steps to take + self.SIM_STEPS = 10000 # simulation steps to take self.DT = 0.001 # timestep [s] # Software stuff diff --git a/WooferRobot.py b/WooferRobot.py index 43df812..a9785f0 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -109,6 +109,7 @@ def step(self, sim): self.step_locations, self.p_step_locations, self.phase) + # ################################### Swing leg control ################################### self.swing_torques, \ self.swing_forces,\ @@ -201,13 +202,13 @@ def MakeWoofer(dt = 0.001): """ Create robot object """ - trot_overlap_time = .05 + trot_overlap_time = .105 trot_step_time = .3 - MPC_horizon = 10 + MPC_horizon = 16 MPC_planning_timestep = 0.01 - MPC_desired_velocity = np.array([0.3, 0.0, 0.0]) + MPC_desired_velocity = np.array([0.305, 0.0, 0.0]) mujoco_state_est = MuJoCoStateEstimator() mujoco_contact_est = MuJoCoContactEstimator() From 916c8c5f695ef86cedf5d9a047f590fc343eef03 Mon Sep 17 00:00:00 2001 From: Tarun Date: Sun, 2 Jun 2019 23:07:21 -0700 Subject: [PATCH 15/17] Took out hacky things and works with noise/latency --- TrajectoryGeneration.py | 4 ++-- WooferConfig.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index 97408e9..9a70a52 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -12,8 +12,8 @@ def __init__(self, gait, dt, N): insert stuff """ self.J = WOOFER_CONFIG.INERTIA - # self.m = WOOFER_CONFIG.MASS # 7.172 - self.m = 7.9 + self.m = WOOFER_CONFIG.MASS # 7.172 + # self.m = 7.9 self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) diff --git a/WooferConfig.py b/WooferConfig.py index 84bbdfc..7e0cc2d 100644 --- a/WooferConfig.py +++ b/WooferConfig.py @@ -28,14 +28,14 @@ def __init__(self): self.INERTIA[1,1] = Iy self.INERTIA[2,2] = Iz - self.JOINT_NOISE = 0#.5 # Nm, 1 sigma of gaussian noise - self.LATENCY = 0 # ms of sense->control latency + self.JOINT_NOISE = 0.5 # Nm, 1 sigma of gaussian noise + self.LATENCY = 2 # ms of sense->control latency self.UPDATE_PERIOD = 1 # ms between control updates class EnvironmentConfig: def __init__(self): self.MU = 1.5 # coeff friction - self.SIM_STEPS = 10000 # simulation steps to take + self.SIM_STEPS = 5000 # simulation steps to take self.DT = 0.001 # timestep [s] # Software stuff From 5d22136bc4ad74ab26dc4ea0d9959a3d8a1c3dc6 Mon Sep 17 00:00:00 2001 From: Tarun Date: Tue, 16 Jul 2019 14:29:18 -0700 Subject: [PATCH 16/17] Added gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store From 83993f9ec8f5f66e9ba71571cc8dbaf07a513d35 Mon Sep 17 00:00:00 2001 From: Tarun Date: Tue, 16 Jul 2019 14:34:03 -0700 Subject: [PATCH 17/17] Minor tweaks to structure and gait params --- BoundGait.py | 111 ++++++++++++++++++++++++++++++++++++++++ MPCPlanner.py | 58 ++++++++++----------- StandingGait.py | 27 ++++++++++ TrajectoryGeneration.py | 24 ++++++++- TrotGait.py | 10 ++-- WooferRobot.py | 28 ++++++++-- woofer_numpy_log.npz | Bin 1417754 -> 8380 bytes 7 files changed, 221 insertions(+), 37 deletions(-) create mode 100644 BoundGait.py create mode 100644 StandingGait.py diff --git a/BoundGait.py b/BoundGait.py new file mode 100644 index 0000000..a2be8a5 --- /dev/null +++ b/BoundGait.py @@ -0,0 +1,111 @@ +import numpy as np +from WooferConfig import WOOFER_CONFIG +from WooferDynamics import FootSelector, CoordinateExpander + +class BoundGait: + def __init__(self, step_time, flight_time): + """ + [FR, FL, BR, BL] + + Four Phases: + Phase 1: BR/BL on ground + Phase 2: no feet on ground + Phase 3: FR/FL on ground + Phase 4: no feet on ground + repeat + Length of Phase 1/3: step_time + Length of Phase 2/4: flight_time + """ + self.step_time = step_time + self.flight_time = flight_time + + self.phase_length = 2*step_time + 2*flight_time + + def getPhase(self, t): + phase_time = t % self.phase_length + + if phase_time < self.step_time: + return 1 + elif phase_time < (self.step_time + self.flight_time): + return 2 + elif phase_time < (2*self.step_time + self.flight_time): + return 3 + else: + return 4 + + def feetInContact(self, phase): + if phase == 1: + return np.array([0, 0, 1, 1]) + elif phase == 2: + return np.zeros(4) + elif phase == 3: + return np.array([1, 1, 0, 0]) + else: + return np.zeros(4) + + def updateStepLocations(self, state, step_locations, p_step_locations, phase): + """ + uses the heuristic in eq. 33 of the MIT Cheetah 3 MPC paper + to calculate the footstep locations + + todo: update step locations based off where they actually land + + side: right leg = 1, left leg = 0 + """ + p_diff = 2.5 * state['p_d'][0:2] * self.step_time + + new_step_locations = step_locations + new_p_step_locations = p_step_locations + + if phase == 1: + # need to move the FR/FL feet + new_step_locations[0:2] = p_step_locations[0:2] + p_diff + new_step_locations[3:5] = p_step_locations[3:5] + p_diff + new_p_step_locations[6:8] = step_locations[6:8] + new_p_step_locations[9:11] = step_locations[9:11] + + elif phase == 3: + # need to move the BR/BL feet + new_step_locations[6:8] = p_step_locations[6:8] + p_diff + new_step_locations[9:11] = p_step_locations[9:11] + p_diff + new_p_step_locations[0:2] = step_locations[0:2] + new_p_step_locations[3:5] = step_locations[3:5] + + return (new_step_locations, new_p_step_locations) + + def getStepPhase(self, t, phase): + """ + returns step phase for swing leg controller + """ + phase_time = t % self.phase_length + + time_into_traj = 0 + if phase == 4: + time_into_traj = phase_time - (self.step_time + 2*self.flight_time) + elif phase == 2: + time_into_traj = phase_time - self.flight_time + return time_into_traj/self.step_time + + def constuctFutureFootHistory(self, t, state, current_foot_locations, step_locations, N, mpc_dt): + """ + + """ + foot_hist = np.zeros((N, 12)) + + future_step_locations = step_locations - CoordinateExpander(state['p']) + + t_i = t + phase_i = self.getPhase(t_i) + prev_phase = phase_i + foot_hist[0, :] = FootSelector(self.feetInContact(phase_i)) * current_foot_locations + + for i in range(1,N): + if(phase_i != prev_phase): + foot_hist[i, :] = FootSelector(self.feetInContact(phase_i)) * future_step_locations + else: + foot_hist[i, :] = foot_hist[i-1, :] + prev_phase = phase_i + t_i += mpc_dt + phase_i = self.getPhase(t_i) + + return foot_hist diff --git a/MPCPlanner.py b/MPCPlanner.py index 8f6428c..1ec8482 100644 --- a/MPCPlanner.py +++ b/MPCPlanner.py @@ -9,8 +9,9 @@ def __init__(self, N, dt_plan): self.dt = dt_plan class MPCWalkingPlanner(MPCPlanner): - def __init__(self, N, dt_plan, gait, init_state, desired_velocity): + def __init__(self, N, dt_plan, dt_torque, gait, init_state, desired_velocity): self.dt = dt_plan + self.dt_torque = dt_torque self.N = N self.gait = gait self.trajectoryGenerator = TrajectoryGeneration(gait, dt_plan, N) @@ -24,30 +25,29 @@ def update(self, state, qpos_joints, foot_locations, t): feet_forces = U_horizon[:12] feet_forces[np.abs(feet_forces) < 1e-5] = 0 - #ground = feet_forces > 0.1 - #feet_forces = np.zeros(12) - #feet_forces[ground] = 7.172*9.81/ground.sum() - joint_torques = np.zeros(12) - - rotmat = rotations.euler2mat(state[3:6]) - - for f in range(4): - # Extract the foot force vector for foot i - foot_force_world = feet_forces[f*3 : f*3 + 3] - - # Transform from world to body frames, - # The negative sign makes it the force exerted by the body - foot_force_body = -np.dot(rotmat.T, foot_force_world) - (beta,theta,r) = tuple(qpos_joints[f*3 : f*3 + 3]) - - # Transform from body frame forces into joint torques - joint_torques[f*3 : f*3 + 3] = np.dot(LegJacobian(beta, theta, r).T, foot_force_body) + # joint_torques = np.zeros(12) + # + # rotmat = rotations.euler2mat(state[3:6]) + # + # for f in range(4): + # # Extract the foot force vector for foot i + # foot_force_world = feet_forces[f*3 : f*3 + 3] + # + # # Transform from world to body frames, + # # The negative sign makes it the force exerted by the body + # foot_force_body = -np.dot(rotmat.T, foot_force_world) + # (beta,theta,r) = tuple(qpos_joints[f*3 : f*3 + 3]) + # + # # Transform from body frame forces into joint torques + # joint_torques[f*3 : f*3 + 3] = np.dot(LegJacobian(beta, theta, r).T, foot_force_body) # print("Feet forces: ", feet_forces) # print("Angular velocity: ", state[9:12]) - return joint_torques + # return joint_torques + + return feet_forces def generateReferenceTrajectory(self, state, t): scale = np.linspace(0, 1, self.N)[:, np.newaxis] @@ -80,9 +80,9 @@ def __init__(self, N, dt_plan, gait, init_state): self.init_state = init_state def generateReferenceTrajectory(self, state, t): - ROLL_CONSTANT = 0.0 + ROLL_CONSTANT = 0.05 PITCH_CONSTANT = 0.0 - YAW_CONSTANT = 0.025 + YAW_CONSTANT = 0 w = 2 * np.pi * 10 scale = np.linspace(0, 1, self.N)[:, np.newaxis] @@ -104,13 +104,13 @@ def generateReferenceTrajectory(self, state, t): # Set z, angle velocities to 0 # They are already 0 - times = np.linspace(t, t + (self.N - 1) * self.dt, self.N) - ref[:, 3] = ROLL_CONSTANT * np.sin(w * times) - ref[:, 4] = PITCH_CONSTANT * np.sin(w * times) - ref[:, 5] = YAW_CONSTANT * np.sin(w * times) - ref[:, 9] = w * ROLL_CONSTANT * np.cos(w * times) - ref[:, 10] = w * PITCH_CONSTANT * np.cos(w * times) - ref[:, 11] = w * YAW_CONSTANT * np.cos(w * times) + times = np.linspace(t + self.dt, t + (self.N - 1) * self.dt, self.N-1) + ref[1:, 3] = ROLL_CONSTANT * np.sin(w * times) + ref[1:, 4] = PITCH_CONSTANT * np.sin(w * times) + ref[1:, 5] = YAW_CONSTANT * np.sin(w * times) + ref[1:, 9] = w * ROLL_CONSTANT * np.cos(w * times) + ref[1:, 10] = w * PITCH_CONSTANT * np.cos(w * times) + ref[1:, 11] = w * YAW_CONSTANT * np.cos(w * times) # if t > 2.0: # import pdb; pdb.set_trace() diff --git a/StandingGait.py b/StandingGait.py new file mode 100644 index 0000000..170e94b --- /dev/null +++ b/StandingGait.py @@ -0,0 +1,27 @@ +import numpy as np +from WooferConfig import WOOFER_CONFIG +from WooferDynamics import FootSelector, CoordinateExpander + +class StandingGait: + def getPhase(self, t): + return 1 + + def feetInContact(self, phase): + return np.ones(4) + + def updateStepLocations(self, state, step_locations, p_step_locations, phase): + return (step_locations, p_step_locations) + + def getStepPhase(self, t, phase): + """ + returns step phase for swing leg controller + """ + return 0 + + def constuctFutureFootHistory(self, t, state, current_foot_locations, step_locations, N, mpc_dt): + """ + + """ + foot_hist = np.tile(current_foot_locations, (N,1)) + + return foot_hist diff --git a/TrajectoryGeneration.py b/TrajectoryGeneration.py index 9a70a52..9be96d6 100644 --- a/TrajectoryGeneration.py +++ b/TrajectoryGeneration.py @@ -13,7 +13,7 @@ def __init__(self, gait, dt, N): """ self.J = WOOFER_CONFIG.INERTIA self.m = WOOFER_CONFIG.MASS # 7.172 - # self.m = 7.9 + # self.m = 7.172 self.C = np.zeros((1,13)) self.D = np.zeros((1,12)) @@ -27,7 +27,25 @@ def __init__(self, gait, dt, N): self.dt = dt self.Q = np.eye(13) - self.alpha = 0 + + # tuning params + + self.Q[0,0] = 1 + self.Q[1,1] = 1 + + # z position + # self.Q[2,2] = 100 + + # roll + # self.Q[3,3] = 4 + + # pitch + # self.Q[3,3] = 10 + + # yaw = + # self.Q[5,5] = 100 + + self.alpha = 1e-6 self.R = self.alpha*np.eye(12) self.mu = 1.5 @@ -138,6 +156,8 @@ def solveSystem(self, refTrajectory, foot_hist, t): def makeConstraints(self, t): """ create constraint matrix C and lower and upper bounds + + need to check these """ curr_t = t diff --git a/TrotGait.py b/TrotGait.py index 2f742d8..9210da8 100644 --- a/TrotGait.py +++ b/TrotGait.py @@ -5,6 +5,8 @@ class TrotGait: def __init__(self, step_time, overlap_time): """ + [FR, FL, BR, BL] + Four Phases: Phase 1: All feet on ground Phase 2: FR/BL on ground @@ -49,12 +51,11 @@ def updateStepLocations(self, state, step_locations, p_step_locations, phase): to calculate the footstep locations todo: update step locations based off where they actually land + (right now it assumes that foot locations end up close to desired) side: right leg = 1, left leg = 0 """ - p_diff = 2.5 * state['p_d'][0:2] * self.step_time - - print(p_diff) + p_diff = 2.625 * state['p_d'][0:2] * self.step_time # if self.isFirstStep: # p_diff = np.array([0.15, 0.0]) @@ -103,6 +104,9 @@ def constuctFutureFootHistory(self, t, state, current_foot_locations, step_locat """ foot_hist = np.zeros((N, 12)) + + # doesn't really make sense - probably should be based off of reference trajectory + # should move this function to MPCPlanner class future_step_locations = step_locations - CoordinateExpander(state['p']) t_i = t diff --git a/WooferRobot.py b/WooferRobot.py index a9785f0..45f8827 100644 --- a/WooferRobot.py +++ b/WooferRobot.py @@ -15,6 +15,8 @@ from ContactEstimator import MuJoCoContactEstimator from SwingLegController import PDSwingLegController, ZeroSwingLegController from TrotGait import TrotGait +from StandingGait import StandingGait +from BoundGait import BoundGait from MPCPlanner import MPCWalkingPlanner, MPCOrientationPlanner from WooferConfig import WOOFER_CONFIG, QP_CONFIG, SWING_CONTROLLER_CONFIG, GAIT_PLANNER_CONFIG @@ -134,7 +136,15 @@ def step(self, sim): self.step_locations, self.gait_planner.N, self.gait_planner.dt) - self.mpc_torques = self.gait_planner.update(state, self.state['j'], self.foot_hist, self.t) + self.foot_forces = self.gait_planner.update(state, self.state['j'], self.foot_hist, self.t) + + # update the torques via jacobian at different rate + if(self.i % (self.gait_planner.dt_torque/self.dt) == 0): + for i in range(4): + # import pdb; pdb.set_trace() + f_i = -self.foot_forces[(3*i):(3*i+3)][np.newaxis].T + self.mpc_torques[(3*i):(3*i+3)] = WooferDynamics.FootForceToJointTorques(f_i, self.state['j'][(3*i):(3*i+3)], self.state['q'], 0)[:,0] + # Expanded version of active feet active_feet_12 = self.active_feet[[0,0,0,1,1,1,2,2,2,3,3,3]] @@ -205,15 +215,27 @@ def MakeWoofer(dt = 0.001): trot_overlap_time = .105 trot_step_time = .3 + bound_step_time = 0.05 + bound_flight_time = 0.01 + MPC_horizon = 16 MPC_planning_timestep = 0.01 + MPC_torque_timestep = 0.001 + + # Trot: + # MPC_desired_velocity = np.array([0.305, 0.0, 0.0]) + + MPC_desired_velocity = np.array([0.3, 0, 0.0]) - MPC_desired_velocity = np.array([0.305, 0.0, 0.0]) + # Bound + # MPC_desired_velocity = np.array([0.0, 0.0, 0.0]) mujoco_state_est = MuJoCoStateEstimator() mujoco_contact_est = MuJoCoContactEstimator() + # gait = BoundGait(bound_step_time, bound_flight_time) gait = TrotGait(trot_step_time, trot_overlap_time) - gait_planner = MPCWalkingPlanner(MPC_horizon, MPC_planning_timestep, + # gait = StandingGait() + gait_planner = MPCWalkingPlanner(MPC_horizon, MPC_planning_timestep, MPC_torque_timestep, gait, np.array([0, 0, (WOOFER_CONFIG.LEG_L + WOOFER_CONFIG.FOOT_RADIUS), 0, 0, 0, 0, 0, 0, 0, 0, 0]), MPC_desired_velocity) # gait_planner = MPCOrientationPlanner(MPC_horizon, MPC_planning_timestep, diff --git a/woofer_numpy_log.npz b/woofer_numpy_log.npz index a1a77421a970860673a22ba276baa55365c2e274..5d1f1c81b31cb2e86fb05836bbeea2a4228f1d08 100644 GIT binary patch literal 8380 zcmWIWW@Zs#fB;2?NOr#c4lE1|AS?tFEXgk_EKQBi$Sel3D)sUTDkuAe`UXTYGL$h? ztEZ$ECl{$(DX80|S*YtMsHf!@l@ulB#pf5Lq=LlV5_5`Ef#Ss(i3O=ZzJ{TZj)I|q zrjA0b0-1md>eNwcGz3ONU0f zz{tP=!k|V^a(-S(Vsc3_)fzY^l(ugaMjJR3CL`Q2E)Fyq7~7d;sqty4sU@S4PGLw7 zBJHS=j@*lp0J^fcJTosnezY$$h$3v%DWf4UY+!!4+bnWPa#t7~A zIj|2Q<1+!&GlYQ##*4t=UK~S`=!T#VY9S0c#shEnVNBQKGX>QtHo&04(PhPF2rQaF w&S_xWC<-$KxDWuT35w4IP$dEb4UEcC*s7EOZ&o&t8V(>7W@2ENEC=EN0D5bnasU7T literal 1417754 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 M&O#$ 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