|
| 1 | +import os |
| 2 | +from functools import partial |
| 3 | + |
| 4 | +import jax |
| 5 | +import jax.numpy as jnp |
| 6 | +import mujoco |
| 7 | +from mujoco import mjx |
| 8 | +from mujoco.mjx._src import math |
| 9 | +from mujoco.mjx._src.dataclasses import PyTreeNode |
| 10 | + |
| 11 | +from mpx.jax_ocp_solvers.jax_ocp_solvers import optimizers |
| 12 | + |
| 13 | + |
| 14 | +dir_path = os.path.dirname(os.path.realpath(__file__)) |
| 15 | +model_path = os.path.abspath(os.path.join(dir_path, "..")) + "/data/go2/go2_mjx.xml" |
| 16 | + |
| 17 | +contact_frame = ["FL", "FR", "RL", "RR"] |
| 18 | +body_name = ["FL_calf", "FR_calf", "RL_calf", "RR_calf"] |
| 19 | + |
| 20 | +dt = 0.02 |
| 21 | +N = 12 |
| 22 | +mpc_frequency = 50.0 |
| 23 | +solver_mode = "equality" |
| 24 | +equality_num_alpha = 5 |
| 25 | + |
| 26 | +timer_t = jnp.array([0.5, 0.0, 0.0, 0.5]) |
| 27 | +duty_factor = jnp.array([0.65]) |
| 28 | +step_freq = jnp.array([1.35]) |
| 29 | +step_height = jnp.array([0.065]) |
| 30 | +initial_height = 0.1 |
| 31 | +robot_height = 0.27 |
| 32 | +use_terrain_estimation = False |
| 33 | + |
| 34 | +p0 = jnp.array([0.0, 0.0, robot_height]) |
| 35 | +quat0 = jnp.array([1.0, 0.0, 0.0, 0.0]) |
| 36 | +q0 = jnp.array([0.0, 0.9, -1.8, 0.0, 0.9, -1.8, 0.0, 0.9, -1.8, 0.0, 0.9, -1.8]) |
| 37 | +q0_init = q0 |
| 38 | +p_legs0 = jnp.array( |
| 39 | + [ |
| 40 | + 0.192, |
| 41 | + 0.142, |
| 42 | + 0.0, |
| 43 | + 0.192, |
| 44 | + -0.142, |
| 45 | + 0.0, |
| 46 | + -0.195, |
| 47 | + 0.142, |
| 48 | + 0.0, |
| 49 | + -0.195, |
| 50 | + -0.142, |
| 51 | + 0.0, |
| 52 | + ] |
| 53 | +) |
| 54 | + |
| 55 | +n_joints = 12 |
| 56 | +n_contact = len(contact_frame) |
| 57 | +nq = 7 + n_joints |
| 58 | +nv = 6 + n_joints |
| 59 | +nx_error = 2 * nv |
| 60 | +n = nq + nv |
| 61 | +m = nv + n_joints + 3 * n_contact |
| 62 | +equality_dim = nv |
| 63 | + |
| 64 | +qacc_slice = slice(0, nv) |
| 65 | +tau_slice = slice(nv, nv + n_joints) |
| 66 | +grf_slice = slice(nv + n_joints, m) |
| 67 | + |
| 68 | +Qp = jnp.diag(jnp.array([0.0, 0.0, 1.0e4])) |
| 69 | +Qrot = jnp.diag(jnp.array([1.0e3, 1.0e3, 0.0])) |
| 70 | +Qq = jnp.diag(jnp.ones(n_joints)) * 1.0e0 |
| 71 | +Qdp = jnp.diag(jnp.array([1.0, 1.0, 1.0])) * 5.0e3 |
| 72 | +Qomega = jnp.diag(jnp.array([1.0, 1.0, 1.0])) * 1.0e2 |
| 73 | +Qdq = jnp.diag(jnp.ones(n_joints)) * 1.0e0 |
| 74 | +Qacc = jnp.diag(jnp.ones(nv)) * 1.0e-1 |
| 75 | +Qtau = jnp.diag(jnp.ones(n_joints)) * 1.0e-3 |
| 76 | +Qgrf = jnp.diag(jnp.ones(3 * n_contact)) * 1.0e-5 |
| 77 | +W = jax.scipy.linalg.block_diag(Qp, Qrot, Qq, Qdp, Qomega, Qdq, Qacc, Qtau, Qgrf) |
| 78 | + |
| 79 | +initial_state = jnp.concatenate([p0, quat0, q0, jnp.zeros(nv)]) |
| 80 | + |
| 81 | +max_torque = 30.0 |
| 82 | +min_torque = -30.0 |
| 83 | +u_ref = jnp.zeros(m) |
| 84 | + |
| 85 | +_MODEL = mujoco.MjModel.from_xml_path(model_path) |
| 86 | +_MODEL.opt.timestep = dt |
| 87 | +_MJX_MODEL = mjx.put_model(_MODEL) |
| 88 | +_CONTACT_ID = [ |
| 89 | + mjx.name2id(_MJX_MODEL, mujoco.mjtObj.mjOBJ_GEOM, name) |
| 90 | + for name in contact_frame |
| 91 | +] |
| 92 | +_BODY_ID = [ |
| 93 | + mjx.name2id(_MJX_MODEL, mujoco.mjtObj.mjOBJ_BODY, name) |
| 94 | + for name in body_name |
| 95 | +] |
| 96 | + |
| 97 | + |
| 98 | +def _state_parts(x): |
| 99 | + qpos = x[:nq] |
| 100 | + qvel = x[nq : nq + nv] |
| 101 | + return qpos, qvel |
| 102 | + |
| 103 | + |
| 104 | +def _integrate_state(qpos, qvel, qacc): |
| 105 | + qvel_next = qvel + qacc * dt |
| 106 | + qpos_next = jnp.concatenate( |
| 107 | + [ |
| 108 | + qpos[:3] + qvel_next[:3] * dt, |
| 109 | + math.quat_integrate(qpos[3:7], qvel_next[3:6], dt), |
| 110 | + qpos[7:] + qvel_next[6:] * dt, |
| 111 | + ] |
| 112 | + ) |
| 113 | + return qpos_next, qvel_next |
| 114 | + |
| 115 | + |
| 116 | +def dynamics(x, u, t, parameter): |
| 117 | + del t, parameter |
| 118 | + qpos, qvel = _state_parts(x) |
| 119 | + qacc = u[qacc_slice] |
| 120 | + qpos_next, qvel_next = _integrate_state(qpos, qvel, qacc) |
| 121 | + return jnp.concatenate([qpos_next, qvel_next]) |
| 122 | + |
| 123 | +def equality(x, u, t, parameter): |
| 124 | + del t |
| 125 | + qpos, qvel = _state_parts(x) |
| 126 | + qacc = u[qacc_slice] |
| 127 | + tau = u[tau_slice] |
| 128 | + grf = u[grf_slice] |
| 129 | + data = mjx.make_data(_MODEL) |
| 130 | + data = data.replace(qpos=qpos, qvel=qvel, qacc=qacc) |
| 131 | + data = mjx.fwd_position(_MJX_MODEL, data) |
| 132 | + data = mjx.fwd_velocity(_MJX_MODEL, data) |
| 133 | + M = data.qM |
| 134 | + D = data.qfrc_bias |
| 135 | + qfrc_inverse = M @ qacc + D |
| 136 | + jacobians = [] |
| 137 | + for contact_id, body_id in zip(_CONTACT_ID, _BODY_ID): |
| 138 | + jac, _ = mjx.jac(_MJX_MODEL, data, data.geom_xpos[contact_id], body_id) |
| 139 | + jacobians.append(jac) |
| 140 | + contact_jacobian = jnp.concatenate(jacobians, axis=1) |
| 141 | + contact_wrench = contact_jacobian @ grf |
| 142 | + generalized_actuation = jnp.concatenate([jnp.zeros(6, dtype=u.dtype), tau]) |
| 143 | + return qfrc_inverse + contact_wrench - generalized_actuation |
| 144 | + |
| 145 | + |
| 146 | +def cost(W, reference, x, u, t): |
| 147 | + qpos, qvel = _state_parts(x) |
| 148 | + p = qpos[:3] |
| 149 | + quat = qpos[3:7] |
| 150 | + q = qpos[7:] |
| 151 | + dp = qvel[:3] |
| 152 | + omega = qvel[3:6] |
| 153 | + dq = qvel[6:] |
| 154 | + acc = u[qacc_slice] |
| 155 | + tau = u[tau_slice] |
| 156 | + grf = u[grf_slice] |
| 157 | + |
| 158 | + p_ref = reference[t, :3] |
| 159 | + quat_ref = reference[t, 3:7] |
| 160 | + q_ref = reference[t, 7 : 7 + n_joints] |
| 161 | + dp_ref = reference[t, nq : nq + 3] |
| 162 | + omega_ref = reference[t, nq + 3 : nq + 6] |
| 163 | + dq_ref = reference[t, nq + 6 : nq + nv] |
| 164 | + |
| 165 | + quat_err = math.quat_sub(quat, quat_ref) |
| 166 | + stage_cost = ( |
| 167 | + (p - p_ref).T @ W[:3, :3] @ (p - p_ref) |
| 168 | + + quat_err.T @ W[3:6, 3:6] @ quat_err |
| 169 | + + (q - q_ref).T @ W[6 : 6 + n_joints, 6 : 6 + n_joints] @ (q - q_ref) |
| 170 | + + (dp - dp_ref).T |
| 171 | + @ W[6 + n_joints : 9 + n_joints, 6 + n_joints : 9 + n_joints] |
| 172 | + @ (dp - dp_ref) |
| 173 | + + (omega - omega_ref).T |
| 174 | + @ W[9 + n_joints : 12 + n_joints, 9 + n_joints : 12 + n_joints] |
| 175 | + @ (omega - omega_ref) |
| 176 | + + (dq - dq_ref).T |
| 177 | + @ W[12 + n_joints : 12 + 2 * n_joints, 12 + n_joints : 12 + 2 * n_joints] |
| 178 | + @ (dq - dq_ref) |
| 179 | + + acc.T @ W[nx_error : nx_error + nv, nx_error : nx_error + nv] @ acc |
| 180 | + + (tau ).T |
| 181 | + @ W[nx_error + nv : nx_error + nv + n_joints, nx_error + nv : nx_error + nv + n_joints] |
| 182 | + @ (tau) |
| 183 | + + (grf).T |
| 184 | + @ W[nx_error + nv + n_joints :, nx_error + nv + n_joints :] |
| 185 | + @ (grf) |
| 186 | + ) |
| 187 | + terminal_cost = ( |
| 188 | + (p - p_ref).T @ W[:3, :3] @ (p - p_ref) |
| 189 | + + quat_err.T @ W[3:6, 3:6] @ quat_err |
| 190 | + + (q - q_ref).T @ W[6 : 6 + n_joints, 6 : 6 + n_joints] @ (q - q_ref) |
| 191 | + + (dp - dp_ref).T |
| 192 | + @ W[6 + n_joints : 9 + n_joints, 6 + n_joints : 9 + n_joints] |
| 193 | + @ (dp - dp_ref) |
| 194 | + + (omega - omega_ref).T |
| 195 | + @ W[9 + n_joints : 12 + n_joints, 9 + n_joints : 12 + n_joints] |
| 196 | + @ (omega - omega_ref) |
| 197 | + + (dq - dq_ref).T |
| 198 | + @ W[12 + n_joints : 12 + 2 * n_joints, 12 + n_joints : 12 + 2 * n_joints] |
| 199 | + @ (dq - dq_ref) |
| 200 | + ) |
| 201 | + return jnp.where(t == N, 0.5 * terminal_cost, 0.5 * stage_cost) |
| 202 | + |
| 203 | + |
| 204 | +def hessian_approx(W, reference, x, u, t): |
| 205 | + del reference, x, u |
| 206 | + state_diag = jnp.concatenate( |
| 207 | + [ |
| 208 | + jnp.diag(W[:3, :3]), |
| 209 | + jnp.ones(4) * 1.0e-3, |
| 210 | + jnp.diag(W[6 : 6 + n_joints, 6 : 6 + n_joints]), |
| 211 | + jnp.diag(W[6 + n_joints : 12 + 2 * n_joints, 6 + n_joints : 12 + 2 * n_joints]), |
| 212 | + ] |
| 213 | + ) |
| 214 | + control_diag = jnp.concatenate( |
| 215 | + [ |
| 216 | + jnp.diag(W[nx_error : nx_error + nv, nx_error : nx_error + nv]), |
| 217 | + jnp.diag( |
| 218 | + W[ |
| 219 | + nx_error + nv : nx_error + nv + n_joints, |
| 220 | + nx_error + nv : nx_error + nv + n_joints, |
| 221 | + ] |
| 222 | + ), |
| 223 | + jnp.diag(W[nx_error + nv + n_joints :, nx_error + nv + n_joints :]), |
| 224 | + ] |
| 225 | + ) |
| 226 | + control_diag = jnp.where(t == N, jnp.ones(m) * 1.0e-6, control_diag) |
| 227 | + |
| 228 | + Q = jnp.diag(state_diag) |
| 229 | + R = jnp.diag(control_diag) |
| 230 | + M = jnp.zeros((n, m), dtype=W.dtype) |
| 231 | + return Q, R, M |
| 232 | + |
| 233 | + |
| 234 | +class InverseDynamicsMPCData(PyTreeNode): |
| 235 | + dt: float |
| 236 | + duty_factor: jnp.ndarray |
| 237 | + step_freq: jnp.ndarray |
| 238 | + step_height: jnp.ndarray |
| 239 | + contact_time: jnp.ndarray |
| 240 | + liftoff: jnp.ndarray |
| 241 | + X0: jnp.ndarray |
| 242 | + U0: jnp.ndarray |
| 243 | + V0: jnp.ndarray |
| 244 | + Veq0: jnp.ndarray |
| 245 | + W: jnp.ndarray |
| 246 | + |
| 247 | + |
| 248 | +@partial(jax.jit, static_argnums=(0, 1)) |
| 249 | +def _update_warm_start(horizon, shift, u_ref, x0, X_prev, U_prev, X, U, V, Veq): |
| 250 | + q_slice = slice(7, 7 + n_joints) |
| 251 | + dq_slice = slice(nq + 6, nq + nv) |
| 252 | + u_fallback_idx = 1 if horizon > 1 else 0 |
| 253 | + |
| 254 | + def shift_trajectory(trajectory): |
| 255 | + tail = jnp.repeat(trajectory[-1:], shift, axis=0) |
| 256 | + return jnp.concatenate([trajectory[shift:], tail], axis=0) |
| 257 | + |
| 258 | + def safe_update(): |
| 259 | + return ( |
| 260 | + shift_trajectory(U), |
| 261 | + shift_trajectory(X), |
| 262 | + shift_trajectory(V), |
| 263 | + shift_trajectory(Veq), |
| 264 | + X[1, q_slice], |
| 265 | + X[1, dq_slice], |
| 266 | + ) |
| 267 | + |
| 268 | + def unsafe_update(): |
| 269 | + return ( |
| 270 | + jnp.tile(u_ref, (horizon, 1)), |
| 271 | + jnp.tile(x0, (horizon + 1, 1)), |
| 272 | + jnp.zeros_like(X_prev), |
| 273 | + jnp.zeros((horizon, equality_dim), dtype=X_prev.dtype), |
| 274 | + X_prev[1, q_slice], |
| 275 | + X_prev[1, dq_slice], |
| 276 | + ) |
| 277 | + |
| 278 | + valid_solution = jnp.logical_not(jnp.isnan(U[0, 0])) |
| 279 | + return jax.lax.cond(valid_solution, safe_update, unsafe_update) |
| 280 | + |
| 281 | + |
| 282 | +class MPCWrapper: |
| 283 | + def __init__(self, config, limited_memory=False): |
| 284 | + self.config = config |
| 285 | + self.mpc_frequency = config.mpc_frequency |
| 286 | + self.shift = max(1, int(1 / (config.dt * config.mpc_frequency))) |
| 287 | + self.qpos_slice = slice(0, nq) |
| 288 | + self.qvel_slice = slice(nq, nq + nv) |
| 289 | + self.model = mujoco.MjModel.from_xml_path(config.model_path) |
| 290 | + self.model.opt.timestep = config.dt |
| 291 | + self.mjx_model = mjx.put_model(self.model) |
| 292 | + |
| 293 | + self.initial_state = jnp.asarray(config.initial_state) |
| 294 | + self.initial_X0 = jnp.tile(self.initial_state, (config.N + 1, 1)) |
| 295 | + self.initial_U0 = jnp.tile(config.u_ref, (config.N, 1)) |
| 296 | + self.initial_V0 = jnp.zeros((config.N + 1, config.n)) |
| 297 | + self.initial_Veq0 = jnp.zeros((config.N, config.equality_dim)) |
| 298 | + |
| 299 | + self.dynamics = config.dynamics |
| 300 | + solver = partial( |
| 301 | + optimizers.mpc_equality, |
| 302 | + config.cost, |
| 303 | + self.dynamics, |
| 304 | + config.hessian_approx, |
| 305 | + limited_memory, |
| 306 | + equality=config.equality, |
| 307 | + num_alpha=config.equality_num_alpha, |
| 308 | + ) |
| 309 | + |
| 310 | + def solve(reference, parameter, W, x0, X0, U0, V0, Veq0): |
| 311 | + return solver(reference, parameter, W, x0, X0, U0, V0, Veq_in=Veq0) |
| 312 | + |
| 313 | + self._solve = jax.jit(solve) |
| 314 | + self._update_warm_start = partial( |
| 315 | + _update_warm_start, |
| 316 | + config.N, |
| 317 | + self.shift, |
| 318 | + config.u_ref, |
| 319 | + ) |
| 320 | + |
| 321 | + def make_data(self): |
| 322 | + return InverseDynamicsMPCData( |
| 323 | + dt=dt, |
| 324 | + duty_factor=duty_factor, |
| 325 | + step_freq=step_freq, |
| 326 | + step_height=step_height, |
| 327 | + contact_time=timer_t, |
| 328 | + liftoff=jnp.zeros(3 * n_contact), |
| 329 | + X0=self.initial_X0, |
| 330 | + U0=self.initial_U0, |
| 331 | + V0=self.initial_V0, |
| 332 | + Veq0=self.initial_Veq0, |
| 333 | + W=W, |
| 334 | + ) |
| 335 | + |
| 336 | + def _reference(self, x, command): |
| 337 | + p_ref = x[:3] + jnp.array([command[0], command[1], 0.0]) * (N * dt) |
| 338 | + p_ref = p_ref.at[2].set(command[6]) |
| 339 | + qpos_ref = jnp.concatenate([p_ref, x[3:7], q0]) |
| 340 | + dp_ref = jnp.array([command[0], command[1], 0.0]) |
| 341 | + omega_ref = jnp.array([0.0, 0.0, command[5]]) |
| 342 | + dq_ref = jnp.zeros(n_joints) |
| 343 | + x_ref = jnp.concatenate([qpos_ref, dp_ref, omega_ref, dq_ref]) |
| 344 | + return jnp.tile(x_ref, (N + 1, 1)) |
| 345 | + |
| 346 | + def control_output(self, x0, X, U, reference, parameter): |
| 347 | + del x0, X, reference, parameter |
| 348 | + return jnp.clip(U[0, tau_slice], min_torque, max_torque) |
| 349 | + |
| 350 | + def run(self, data, x, command, contact=None): |
| 351 | + del contact |
| 352 | + reference = self._reference(x, command) |
| 353 | + parameter = jnp.zeros((N + 1, 1), dtype=x.dtype) |
| 354 | + X, U, V, Veq = self._solve( |
| 355 | + reference, |
| 356 | + parameter, |
| 357 | + data.W, |
| 358 | + x, |
| 359 | + data.X0, |
| 360 | + data.U0, |
| 361 | + data.V0, |
| 362 | + data.Veq0, |
| 363 | + ) |
| 364 | + valid_solution = jnp.logical_not(jnp.isnan(U[0, 0])) |
| 365 | + tau = jax.lax.cond( |
| 366 | + valid_solution, |
| 367 | + lambda _: self.control_output(x, X, U, reference, parameter), |
| 368 | + lambda _: self.control_output(x, data.X0, data.U0, reference, parameter), |
| 369 | + operand=None, |
| 370 | + ) |
| 371 | + U0, X0, V0, Veq0, q, dq = self._update_warm_start( |
| 372 | + x, |
| 373 | + data.X0, |
| 374 | + data.U0, |
| 375 | + X, |
| 376 | + U, |
| 377 | + V, |
| 378 | + Veq, |
| 379 | + ) |
| 380 | + return data.replace(X0=X0, U0=U0, V0=V0, Veq0=Veq0), tau, q, dq |
| 381 | + |
| 382 | + def reset(self, data, qpos, qvel, foot=None): |
| 383 | + del foot |
| 384 | + x = ( |
| 385 | + self.initial_state.at[self.qpos_slice].set(jnp.ravel(qpos)) |
| 386 | + .at[self.qvel_slice].set(jnp.ravel(qvel)) |
| 387 | + ) |
| 388 | + return data.replace( |
| 389 | + X0=jnp.tile(x, (N + 1, 1)), |
| 390 | + U0=self.initial_U0, |
| 391 | + V0=self.initial_V0, |
| 392 | + Veq0=self.initial_Veq0, |
| 393 | + liftoff=jnp.zeros(3 * n_contact), |
| 394 | + contact_time=timer_t, |
| 395 | + ) |
| 396 | + |
| 397 | + |
| 398 | +def state_to_qpos(x): |
| 399 | + return x[:nq] |
0 commit comments