Skip to content

Commit 264b492

Browse files
committed
Add example of inverse dynamics
1 parent 4e1e8de commit 264b492

1 file changed

Lines changed: 344 additions & 0 deletions

File tree

examples/tsid_example.py

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
"""
2+
Whole-Body QP Controller for Biped Walking using TSID + Pinocchio.
3+
4+
This example shows how to replace differential inverse kinematics with a
5+
torque-level whole-body QP controller. The pipeline becomes:
6+
7+
ZMP Preview Controller -> COM/foot reference trajectories
8+
-> TSID (this code)
9+
-> joint torques
10+
11+
Decision variables: joint accelerations (q_ddot) and contact forces (f_c)
12+
Equality constraint: M * q_ddot + h = S^T * tau + J_c^T * f_c
13+
Inequality constraints: friction cones, torque limits
14+
Cost: weighted COM tracking + foot tracking + posture regularization
15+
16+
Dependencies:
17+
pip install tsid pin numpy example-robot-data
18+
# or: conda install tsid pinocchio example-robot-data -c conda-forge
19+
"""
20+
21+
import argparse
22+
from pathlib import Path
23+
24+
import numpy as np
25+
import tsid
26+
import pinocchio as pin
27+
28+
29+
def set_joint(q, model, joint_name, val):
30+
jid = model.getJointId(joint_name)
31+
if jid > 0 and model.joints[jid].nq == 1:
32+
q[model.joints[jid].idx_q] = val
33+
34+
35+
def make_initial_configuration(model):
36+
q = pin.neutral(model)
37+
38+
# Talos standing pose. Missing joints are ignored so this still works with
39+
# another URDF, where the neutral configuration is kept for unknown joints.
40+
set_joint(q, model, "leg_left_1_joint", 0.0)
41+
set_joint(q, model, "leg_left_2_joint", 0.0)
42+
set_joint(q, model, "leg_left_3_joint", -0.5)
43+
set_joint(q, model, "leg_left_4_joint", 1.0)
44+
set_joint(q, model, "leg_left_5_joint", -0.6)
45+
set_joint(q, model, "leg_right_1_joint", 0.0)
46+
set_joint(q, model, "leg_right_2_joint", 0.0)
47+
set_joint(q, model, "leg_right_3_joint", -0.5)
48+
set_joint(q, model, "leg_right_4_joint", 1.0)
49+
set_joint(q, model, "leg_right_5_joint", -0.6)
50+
set_joint(q, model, "arm_right_4_joint", -1.5)
51+
set_joint(q, model, "arm_left_4_joint", -1.5)
52+
53+
return q
54+
55+
56+
def parse_args():
57+
parser = argparse.ArgumentParser()
58+
parser.add_argument(
59+
"--model-root",
60+
type=Path,
61+
default=Path("/home/rdesarz/projects"),
62+
help="Folder used as the package root for meshes and relative URDF paths.",
63+
)
64+
parser.add_argument(
65+
"--urdf",
66+
type=Path,
67+
default=Path("talos_data/urdf/talos_full.urdf"),
68+
help="URDF path. Relative paths are resolved from --model-root.",
69+
)
70+
return parser.parse_args()
71+
72+
73+
# ============================================================================
74+
# 1. ROBOT SETUP
75+
# ============================================================================
76+
77+
args = parse_args()
78+
model_root = args.model_root.expanduser().resolve()
79+
urdf_path = args.urdf.expanduser()
80+
if not urdf_path.is_absolute():
81+
urdf_path = model_root / urdf_path
82+
urdf_path = urdf_path.resolve()
83+
84+
if not urdf_path.is_file():
85+
raise FileNotFoundError(f"URDF file not found: {urdf_path}")
86+
87+
# Load a humanoid URDF (Talos by default, or pass --urdf for yours).
88+
full_model, full_col_model, full_vis_model = pin.buildModelsFromUrdf(
89+
str(urdf_path), str(model_root), pin.JointModelFreeFlyer()
90+
)
91+
92+
# TSID RobotWrapper: loads URDF with a free-flyer joint (floating base)
93+
robot = tsid.RobotWrapper(str(urdf_path), [str(model_root)], pin.JointModelFreeFlyer(), False)
94+
model = robot.model()
95+
data = robot.data()
96+
97+
# Frame names for Talos (adapt these to your URDF)
98+
LEFT_FOOT_FRAME = "left_sole_link"
99+
RIGHT_FOOT_FRAME = "right_sole_link"
100+
101+
# Initial configuration: half-sitting / standing pose
102+
# Use a Talos standing pose when these joint names exist, otherwise neutral.
103+
q0 = make_initial_configuration(model)
104+
v0 = np.zeros(robot.nv)
105+
106+
# ============================================================================
107+
# 2. SOLVER SETUP
108+
# ============================================================================
109+
110+
dt = 1e-3 # Control timestep (1 kHz)
111+
112+
# Create the inverse dynamics QP formulation
113+
# This sets up: min ||tasks||^2 s.t. dynamics + contacts
114+
invdyn = tsid.InverseDynamicsFormulationAccForce("tsid", robot, False)
115+
invdyn.computeProblemData(0.0, q0, v0)
116+
117+
# ============================================================================
118+
# 3. CONTACT SETUP (both feet on ground in double support)
119+
# ============================================================================
120+
121+
# Contact parameters
122+
mu = 0.5 # Friction coefficient
123+
f_min = 1.0 # Minimum normal force (N) -- prevents "pulling" on ground
124+
f_max = 1000.0 # Maximum normal force (N)
125+
contact_normal = np.array([0.0, 0.0, 1.0]) # Ground normal
126+
127+
# Contact surface corners (rectangular foot, in local foot frame)
128+
# Adapt these dimensions to your robot's foot geometry
129+
lx = 0.10 # half-length along x
130+
ly = 0.05 # half-width along y
131+
contact_points = np.array([
132+
[ lx, ly, 0.0],
133+
[ lx, -ly, 0.0],
134+
[-lx, -ly, 0.0],
135+
[-lx, ly, 0.0],
136+
]).T # Shape: (3, 4)
137+
138+
# Gains for contact stabilization (keeps feet from drifting)
139+
kp_contact = 30.0
140+
kd_contact = 2.0 * np.sqrt(kp_contact)
141+
142+
# Weight for contact force regularization in the cost
143+
w_force_reg = 1e-5
144+
145+
# --- Left foot contact ---
146+
contact_left = tsid.Contact6d(
147+
"contact_left", robot, LEFT_FOOT_FRAME,
148+
contact_points, contact_normal, mu, f_min, f_max
149+
)
150+
contact_left.setKp(kp_contact * np.ones(6))
151+
contact_left.setKd(kd_contact * np.ones(6))
152+
H_left_ref = robot.framePosition(invdyn.data(), model.getFrameId(LEFT_FOOT_FRAME))
153+
contact_left.setReference(H_left_ref)
154+
invdyn.addRigidContact(contact_left, w_force_reg, 1.0, 1)
155+
156+
# --- Right foot contact ---
157+
contact_right = tsid.Contact6d(
158+
"contact_right", robot, RIGHT_FOOT_FRAME,
159+
contact_points, contact_normal, mu, f_min, f_max
160+
)
161+
contact_right.setKp(kp_contact * np.ones(6))
162+
contact_right.setKd(kd_contact * np.ones(6))
163+
H_right_ref = robot.framePosition(invdyn.data(), model.getFrameId(RIGHT_FOOT_FRAME))
164+
contact_right.setReference(H_right_ref)
165+
invdyn.addRigidContact(contact_right, w_force_reg, 1.0, 1)
166+
167+
# ============================================================================
168+
# 4. TASK DEFINITIONS
169+
# ============================================================================
170+
171+
# --- COM tracking task ---
172+
# This is where your ZMP preview controller output goes
173+
kp_com = 30.0
174+
kd_com = 2.0 * np.sqrt(kp_com)
175+
w_com = 1.0 # Weight in the cost
176+
177+
com_task = tsid.TaskComEquality("task-com", robot)
178+
com_task.setKp(kp_com * np.ones(3))
179+
com_task.setKd(kd_com * np.ones(3))
180+
invdyn.addMotionTask(com_task, w_com, 1, 0.0)
181+
182+
# Initial COM reference (will be overwritten by your trajectory generator)
183+
com_ref = robot.com(invdyn.data())
184+
com_traj = tsid.TrajectoryEuclidianConstant("traj-com", com_ref)
185+
186+
# --- Swing foot tracking task (SE3) ---
187+
# Only active during single support; add/remove dynamically
188+
kp_foot = 100.0
189+
kd_foot = 2.0 * np.sqrt(kp_foot)
190+
w_foot = 1.0
191+
192+
swing_foot_task = tsid.TaskSE3Equality("task-swing-foot", robot, RIGHT_FOOT_FRAME)
193+
swing_foot_task.setKp(kp_foot * np.ones(6))
194+
swing_foot_task.setKd(kd_foot * np.ones(6))
195+
# Mask: track all 6 DoF (position + orientation). Set mask[3:6]=0 to ignore rotation.
196+
swing_foot_task.setMask(np.ones(6))
197+
# NOTE: Don't add this task yet -- it's only active during single support.
198+
199+
# --- Posture regularization task ---
200+
# Prevents the QP from producing wild joint motions
201+
kp_posture = 1.0
202+
kd_posture = 2.0 * np.sqrt(kp_posture)
203+
w_posture = 0.1 # Low weight: posture is a soft objective
204+
205+
posture_task = tsid.TaskJointPosture("task-posture", robot)
206+
posture_task.setKp(kp_posture * np.ones(robot.nv - 6)) # -6 for free-flyer
207+
posture_task.setKd(kd_posture * np.ones(robot.nv - 6))
208+
invdyn.addMotionTask(posture_task, w_posture, 1, 0.0)
209+
210+
# Posture reference: the standing configuration (minus free-flyer)
211+
q_posture_ref = q0[7:] # Remove 7 DoF free-flyer (pos + quat)
212+
sample_posture = tsid.TrajectorySample(robot.nv - 6)
213+
sample_posture.pos(q_posture_ref)
214+
215+
# ============================================================================
216+
# 5. QP SOLVER
217+
# ============================================================================
218+
219+
solver = tsid.SolverHQuadProgFast("solver-qp")
220+
solver.resize(invdyn.nVar, invdyn.nEq, invdyn.nIn)
221+
222+
# ============================================================================
223+
# 6. SIMULATION LOOP
224+
# ============================================================================
225+
226+
N_STEPS = 5000 # 5 seconds at 1 kHz
227+
q = q0.copy()
228+
v = v0.copy()
229+
230+
# Storage for logging
231+
com_log = np.zeros((N_STEPS, 3))
232+
tau_log = []
233+
234+
for i in range(N_STEPS):
235+
t = i * dt
236+
237+
# ------------------------------------------------------------------
238+
# 6a. SET REFERENCES FROM YOUR TRAJECTORY GENERATOR
239+
# ------------------------------------------------------------------
240+
# Replace these with your actual ZMP preview controller outputs:
241+
# com_des[t], com_vel_des[t], com_acc_des[t]
242+
# swing_foot_pose_des[t], swing_foot_vel_des[t], swing_foot_acc_des[t]
243+
244+
# Example: constant COM reference (just standing)
245+
sample_com = com_traj.computeNext()
246+
com_task.setReference(sample_com)
247+
248+
# Example: constant posture reference
249+
posture_task.setReference(sample_posture)
250+
251+
# ------------------------------------------------------------------
252+
# 6b. CONTACT SWITCHING (for walking)
253+
# ------------------------------------------------------------------
254+
# During single support, you need to:
255+
# 1. Remove the contact for the swing foot
256+
# 2. Add the swing foot tracking task
257+
#
258+
# Example (not active in this standing demo):
259+
#
260+
# # Entering single support (right foot swings):
261+
# invdyn.removeRigidContact(contact_right.name, transition_duration=0.0)
262+
# invdyn.addMotionTask(swing_foot_task, w_foot, 1, 0.0)
263+
# swing_foot_task.setReference(desired_foot_SE3_sample)
264+
#
265+
# # Entering double support (right foot lands):
266+
# invdyn.removeTask(swing_foot_task.name, transition_duration=0.0)
267+
# H_right_new = <desired landing pose>
268+
# contact_right.setReference(H_right_new)
269+
# invdyn.addRigidContact(contact_right, w_force_reg, 1.0, 1)
270+
271+
# ------------------------------------------------------------------
272+
# 6c. SOLVE THE QP
273+
# ------------------------------------------------------------------
274+
HQP = invdyn.computeProblemData(t, q, v)
275+
sol = solver.solve(HQP)
276+
277+
if sol.status != 0:
278+
print(f"[t={t:.3f}] QP solver failed with status {sol.status}")
279+
break
280+
281+
# Extract joint torques from the solution
282+
tau = invdyn.getActuatorForces(sol)
283+
284+
# Extract joint accelerations
285+
dv = invdyn.getAccelerations(sol)
286+
287+
# ------------------------------------------------------------------
288+
# 6d. INTEGRATE (simple Euler -- replace with your simulator)
289+
# ------------------------------------------------------------------
290+
# In practice, send `tau` to your simulator (PyBullet, MuJoCo, etc.)
291+
# and read back q, v. Here we do a simple forward integration.
292+
v_next = v + dv * dt
293+
q_next = pin.integrate(model, q, v_next * dt)
294+
295+
q = q_next
296+
v = v_next
297+
298+
# ------------------------------------------------------------------
299+
# 6e. LOG
300+
# ------------------------------------------------------------------
301+
com_log[i] = robot.com(invdyn.data())
302+
tau_log.append(tau.copy())
303+
304+
if i % 1000 == 0:
305+
com_pos = robot.com(invdyn.data())
306+
print(f"[t={t:.3f}] COM: [{com_pos[0]:.4f}, {com_pos[1]:.4f}, {com_pos[2]:.4f}]")
307+
308+
309+
print("\nDone. Final COM:", robot.com(invdyn.data()))
310+
print(f"Torque vector dimension: {tau.shape[0]} (actuated joints)")
311+
312+
# ============================================================================
313+
# 7. HELPER: HOW TO SET SE3 REFERENCES FOR SWING FOOT
314+
# ============================================================================
315+
316+
def make_se3_sample(position, orientation_matrix, velocity=None, acceleration=None):
317+
"""
318+
Build a TrajectorySample for TaskSE3Equality from position + rotation.
319+
320+
TSID expects a 12-dim pos vector: [translation(3), rotation_matrix_flat(9)]
321+
and 6-dim vel/acc vectors: [linear(3), angular(3)]
322+
"""
323+
sample = tsid.TrajectorySample(12, 6)
324+
325+
pos = np.zeros(12)
326+
pos[:3] = position
327+
pos[3:] = orientation_matrix.flatten() # Column-major
328+
sample.pos(pos)
329+
330+
if velocity is not None:
331+
sample.vel(velocity) # 6D: [v_linear, omega]
332+
if acceleration is not None:
333+
sample.acc(acceleration)
334+
335+
return sample
336+
337+
338+
# Example usage for a swing foot trajectory point:
339+
#
340+
# foot_pos = np.array([0.0, -0.1, 0.05]) # Foot lifted 5cm
341+
# foot_rot = np.eye(3) # Flat orientation
342+
# foot_vel = np.zeros(6)
343+
# foot_acc = np.zeros(6)
344+
# sample = make_se3_sample(foot_pos, foot_rot, foot_vel, foot_acc)

0 commit comments

Comments
 (0)