Skip to content

Commit 49ba891

Browse files
committed
add inverse dynamic fomulation
1 parent 90bab3a commit 49ba891

21 files changed

Lines changed: 1721 additions & 511 deletions

mpx/config/config_go2_inv.py

Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
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

Comments
 (0)