Files
hand-motion-pipeline/scripts/l20_bottle_physics.py
T

191 lines
12 KiB
Python

"""Actuated L20 + freely simulated metric bottle, with a prescribed moving wrist."""
import os
os.environ.setdefault('MUJOCO_GL','osmesa')
import argparse,json
import xml.etree.ElementTree as ET
from pathlib import Path
import numpy as np
import mujoco
from scipy.spatial.transform import Rotation
from scipy.optimize import lsq_linear
from l20_calibrated import ROOT,Calibration
BASE=ROOT/'output/l20_2047635068'
OUT=BASE/'bottle_grasp'
def feedback_torque(model,data,cal,addr,objects,object_body,target_pos,target_velocity):
selected=[c for c in data.contact if any(g in objects for g in c.geom)]
if not selected:return None,None
center=data.xipos[object_body].copy();velocity=np.zeros(6)
mujoco.mj_objectVelocity(model,data,mujoco.mjtObj.mjOBJ_BODY,object_body,velocity,0)
desiredforce=np.array([0,0,model.body_mass[object_body]*9.81])+100*(target_pos-center)+2*(target_velocity-velocity[3:])
rotation=Rotation.from_matrix(data.xmat[object_body].reshape(3,3))
desiredtorque=-.08*rotation.as_rotvec()-.008*velocity[:3]
target=np.r_[desiredforce,desiredtorque/.03]
columns=[];jacobians=[];rays_all=[];preferred=[]
for c in selected:
object_first=c.geom[0] in objects
normal=-c.frame[:3]*(1 if object_first else -1)
tangent=np.cross(normal,[0,0,1])
if np.linalg.norm(tangent)<.01:tangent=np.cross(normal,[1,0,0])
tangent/=np.linalg.norm(tangent);second=np.cross(normal,tangent)
rays=np.array([normal+.6*(np.cos(a)*tangent+np.sin(a)*second) for a in np.arange(4)*np.pi/2]).T
rays_all.append(rays);columns.append(np.vstack([rays,np.cross((c.pos-center)[None,:],rays.T).T/.03]))
body=int(model.geom_bodyid[c.geom[1] if object_first else c.geom[0]])
preferred.append(6 if model.body(body).name.startswith('thumb') else 1.5)
jac=np.zeros((3,model.nv));mujoco.mj_jac(model,data,jac,None,c.pos,body)
ja=np.stack([jac[:,model.jnt_dofadr[model.joint(n).id]] for n in cal.active],axis=1)
for n,(parent,_,_) in cal.curves.items():
slope=cal.passive_value(n,data.qpos[addr[parent]])[1]
ja[:,cal.active.index(parent)]+=slope*jac[:,model.jnt_dofadr[model.joint(n).id]]
jacobians.append(ja)
A=np.concatenate(columns,axis=1);reg=np.zeros((len(selected),len(selected)*4))
for i in range(len(selected)):reg[i,i*4:i*4+4]=.025
sol=lsq_linear(np.vstack([A,reg]),np.r_[target,.025*np.array(preferred)],bounds=(0,10),tol=1e-6,max_iter=50)
tau=sum(j.T@(r@sol.x[i*4:i*4+4]) for i,(j,r) in enumerate(zip(jacobians,rays_all)))
for i,n in enumerate(cal.active):tau[i]+=data.qfrc_bias[model.jnt_dofadr[model.joint(n).id]]
return tau,float(np.linalg.norm(A@sol.x-target))
def build_physics(kp=30):
tree=ET.parse(BASE/'contact_object/contact_scene.xml');root=tree.getroot()
hand=root.find("worldbody/body[@name='hand_base_link']")
hand.remove(hand.find('freejoint'));hand.set('mocap','true')
option=root.find('option');option.set('timestep','.001');option.set('integrator','implicitfast');option.set('iterations','80')
root.find('default/joint').set('damping','.08')
eq=root.find('equality')
if eq is None:eq=ET.SubElement(root,'equality')
actuator=ET.SubElement(root,'actuator')
cal=Calibration()
for n in cal.active:
ET.SubElement(actuator,'position',name='motor_'+n,joint=n,kp=str(kp),kv='.3',
forcelimited='true',forcerange='-1 1')
for n,(parent,x,y) in cal.curves.items():
ET.SubElement(eq,'joint',name='lut_'+n,joint1=n,joint2=parent,
polycoef='0 1 0 0 0',solref='.003 1',solimp='.95 .99 .001')
path=OUT/'physics_scene.xml';tree.write(path)
return path
def run(seconds=3.,lift=False,kp=30.,preload=0.,force_preload=False,force_control=False,feedback=False):
path=build_physics(kp);model=mujoco.MjModel.from_xml_path(str(path));data=mujoco.MjData(model)
cal=Calibration();old=mujoco.MjModel.from_xml_path(str(BASE/'contact_object/contact_scene.xml'))
fitted=np.load(BASE/'contact_object/contact_result.npz')['qpos']
names=list(cal.tables)
addr={n:model.jnt_qposadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,n)] for n in names}
for n in names:
oldaddr=old.jnt_qposadr[mujoco.mj_name2id(old,mujoco.mjtObj.mjOBJ_JOINT,n)]
data.qpos[addr[n]]=fitted[oldaddr]
ow=old.jnt_qposadr[mujoco.mj_name2id(old,mujoco.mjtObj.mjOBJ_JOINT,'wrist_free')]
oo=old.jnt_qposadr[mujoco.mj_name2id(old,mujoco.mjtObj.mjOBJ_JOINT,'object_free')]
obj=model.jnt_qposadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,'object_free')]
# Rigidly reorient the entire registration so bottle +Z is world up.
rotation=Rotation.from_quat(fitted[oo+3:oo+7][[1,2,3,0]]).as_matrix().T
handpos=rotation@(fitted[ow:ow+3]-fitted[oo:oo+3])+[0,0,.3]
handrot=rotation@Rotation.from_quat(fitted[ow+3:ow+7][[1,2,3,0]]).as_matrix()
data.mocap_pos[0]=handpos;data.mocap_quat[0]=Rotation.from_matrix(handrot).as_quat()[[3,0,1,2]]
data.qpos[obj:obj+7]=[0,0,.3,1,0,0,0]
controls=np.array([data.qpos[addr[n]] for n in cal.active])
reference_controls=controls.copy()
for i,n in enumerate(cal.active):
if n.endswith(('pitch','pip','mcp')):controls[i]+=preload
controls[i]=np.clip(controls[i],*cal.bounds[n])
data.ctrl[:]=controls
objects=np.flatnonzero(model.geom_contype==2)
object_body=mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_BODY,'grasp_object')
preload_report={}
if force_preload:
for n,(parent,x,y) in cal.curves.items():
a=data.qpos[addr[parent]];v,s=cal.passive_value(n,a)
ei=mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_EQUALITY,'lut_'+n)
model.eq_data[ei,:5]=[v-s*a,s,0,0,0]
mujoco.mj_forward(model,data)
selected=[c for c in data.contact if any(g in objects for g in c.geom)]
columns=[];jacobians=[];cone=[]
center=data.xipos[object_body].copy()
for c in selected:
object_first=c.geom[0] in objects
outward=c.frame[:3]*(1 if object_first else -1)
normal=-outward
tangent=np.cross(normal,[0,0,1])
if np.linalg.norm(tangent)<.01:tangent=np.cross(normal,[1,0,0])
tangent/=np.linalg.norm(tangent);second=np.cross(normal,tangent)
rays=np.array([normal+.6*(np.cos(a)*tangent+np.sin(a)*second) for a in np.arange(4)*np.pi/2]).T
cone.append(rays)
columns.append(np.vstack([rays,np.cross((c.pos-center)[None,:],rays.T).T/.03]))
body=model.geom_bodyid[c.geom[1] if object_first else c.geom[0]]
jac=np.zeros((3,model.nv));mujoco.mj_jac(model,data,jac,None,c.pos,int(body))
activejac=np.stack([jac[:,model.jnt_dofadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,n)]] for n in cal.active],axis=1)
for n,(parent,x,y) in cal.curves.items():
slope=cal.passive_value(n,data.qpos[addr[parent]])[1]
di=model.jnt_dofadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,n)]
activejac[:,cal.active.index(parent)]+=slope*jac[:,di]
jacobians.append(activejac)
if not selected:raise RuntimeError('No contacts for force preload')
A=np.concatenate(columns,axis=1)
regular=np.zeros((len(selected),len(selected)*4))
for i in range(len(selected)):regular[i,i*4:i*4+4]=.025
target=np.r_[0,0,model.body_mass[object_body]*9.81,0,0,0]
preferred=np.array([6.]+[1.5]*(len(selected)-1))
solution=lsq_linear(np.vstack([A,regular]),np.r_[target,.025*preferred],bounds=(0,10),tol=1e-10)
tau=np.zeros(16)
for i,jac in enumerate(jacobians):tau+=jac.T@(cone[i]@solution.x[i*4:i*4+4])
for i,n in enumerate(cal.active):
dof=model.jnt_dofadr[mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_JOINT,n)]
tau[i]+=data.qfrc_bias[dof]
controls+=np.clip(tau,-1,1)/kp
for i,n in enumerate(cal.active):controls[i]=np.clip(controls[i],*cal.bounds[n])
data.ctrl[:]=controls
preload_report=dict(wrench_residual=float(np.linalg.norm(A@solution.x-target)),desired_motor_torque_Nm=tau.tolist(),contact_normal_loads_N=(regular@solution.x/.025).tolist())
rows=[];forces=[];contacts=[];relative=[];torques=[];wrist_path=[]
initial_rel=np.array([0,0,.3])-handpos
feedback_errors=[]
for k in range(int(seconds/model.opt.timestep)):
t=k*model.opt.timestep
for n,(parent,x,y) in cal.curves.items():
active_angle=data.qpos[addr[parent]];passive,slope=cal.passive_value(n,active_angle)
eid=mujoco.mj_name2id(model,mujoco.mjtObj.mjOBJ_EQUALITY,'lut_'+n)
model.eq_data[eid,:5]=[passive-slope*active_angle,slope,0,0,0]
displacement=np.zeros(3)
if lift:
phase=np.clip((t-.5)/1.,0,1);smooth=phase*phase*(3-2*phase)
displacement=np.array([.02*smooth,0,.10*smooth])
data.mocap_pos[0]=handpos+displacement
if feedback and k%10==0:
phase=np.clip((t-.5),0,1)
target_velocity=np.array([.02,0,.10])*6*phase*(1-phase) if lift else np.zeros(3)
target_center=np.array([0,0,.3663])+displacement
newtau,residual=feedback_torque(model,data,cal,addr,objects,object_body,target_center,target_velocity)
if newtau is not None:tau=newtau;feedback_errors.append(residual)
if force_control:
if not force_preload:raise ValueError('force-control requires force-preload')
angles=np.array([data.qpos[addr[n]] for n in cal.active])
data.ctrl[:]=angles+(tau+1.0*(reference_controls-angles))/kp
mujoco.mj_step(model,data)
if not np.isfinite(data.qpos).all():raise RuntimeError('Nonfinite simulation')
if k%33==0:
rows.append(data.qpos.copy());wrist_path.append(np.r_[data.mocap_pos[0],data.mocap_quat[0]])
selected=[i for i,c in enumerate(data.contact) if any(g in objects for g in c.geom)]
normalforce=0
for ci in selected:
f=np.zeros(6);mujoco.mj_contactForce(model,data,ci,f);normalforce+=max(0,f[0])
forces.append(normalforce);contacts.append(len(selected));torques.append(data.actuator_force.copy())
relative.append(np.linalg.norm(data.qpos[obj:obj+3]-data.mocap_pos[0]-initial_rel))
rows=np.array(rows);relative=np.array(relative)
report=dict(seconds=seconds,lift=lift,kp=kp,preload_rad=preload,force_preload=preload_report,force_control=force_control,feedback=feedback,
feedback_wrench_residual_mean=float(np.mean(feedback_errors)) if feedback_errors else None,
max_relative_drift_m=float(relative.max()),final_relative_drift_m=float(relative[-1]),
object_vertical_displacement_m=float(data.qpos[obj+2]-.3),
contact_frames=int(np.sum(np.array(contacts)>0)),total_frames=len(rows),
normal_force_mean_N=float(np.mean(forces)),peak_actuator_torque_Nm=float(np.max(np.abs(torques))),
retained=bool(relative.max()<.02),
mass_kg=float(model.body_mass[object_body]),friction=.6,
boundaries=['Prescribed moving wrist, no arm dynamics','16 force-limited active joints; 5 nonlinear LUT equality constraints','No object weld or position reset','Object registration is estimated from hand pose, not tri-camera object tracking'])
np.savez_compressed(OUT/'physics_motion.npz',qpos=rows,wrist=np.array(wrist_path),fps=1/(33*model.opt.timestep),
object_qpos_address=obj,contact_count=contacts,normal_force=forces,relative_drift=relative,actuator_force=torques,
source_to_upright_rotation=rotation,controls=controls)
(OUT/'physics_validation.json').write_text(json.dumps(report,indent=2));print(json.dumps(report,indent=2),flush=True)
return report
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--seconds',type=float,default=3);p.add_argument('--lift',action='store_true');p.add_argument('--kp',type=float,default=30);p.add_argument('--preload',type=float,default=0);p.add_argument('--force-preload',action='store_true');p.add_argument('--force-control',action='store_true');p.add_argument('--feedback',action='store_true');a=p.parse_args()
run(a.seconds,a.lift,a.kp,a.preload,a.force_preload,a.force_control,a.feedback)