A gripper's two fingers usually move together: closing one closes the other. URDF expresses
this with a <mimic joint="..." multiplier="..." offset="..."/> tag inside the slaved joint --
its own angle is always multiplier * master_angle + offset, never a free variable.
RigidBodyModel previously ignored that tag and gave the slaved joint its own independent
coordinate, double-counting a single real degree of freedom.
A joint with a <mimic> tag no longer gets its own entry in q/qd/tau -- its motion is
computed from its master's coordinate wherever the model needs it:
model=RigidBodyModel("panda_arm_hand.urdf.xacro")model.n# 8, not 9 -- the two fingers share one real DOFmodel.mimic_map["panda_finger_joint2"]# (master_dof_idx, multiplier, offset)
Forward kinematics substitutes q[master] * multiplier + offset for the mimic joint's own
angle -- exact, not approximate, so gravity/Coriolis terms obtained through jax.grad/jax.jvp
on the resulting kinematics are automatically correct. The hand-built geometric Jacobian used
for the mass matrix needs its own explicit chain rule: a mimic joint's local Jacobian column
(computed from its own axis and origin, same as any joint) is scaled by multiplier and added
into its master's column, rather than getting a column of its own.
urdf_path : str
Path to a real URDF file. Any kinematic tree of revolute, continuous,
prismatic, or fixed joints -- not assumed to be a single serial chain.
n : int
Number of non-fixed joints (degrees of freedom), in a canonical
depth-first order from the URDF's root link.
q_min, q_max, qd_max : ndarray, shape (n,)
Real per-joint position/velocity limits from the URDF's own
tags (+/-inf wherever the URDF declares none).
Source code in dense_armor/dynamics/urdf_dynamics.py
def__init__(self,urdf_path):links,joints_by_parent,root_link=_parse_urdf(urdf_path)self.links=linksself.root_link=root_linkself.dof_joints=[]self.link_parent_joint={}defwalk(link_name):forjinjoints_by_parent.get(link_name,[]):self.link_parent_joint[j["child"]]=jifj["type"]!="fixed"andj["mimic_joint"]isNone:self.dof_joints.append(j)walk(j["child"])walk(root_link)self.n=len(self.dof_joints)self.dof_index={j["name"]:idxforidx,jinenumerate(self.dof_joints)}# A <mimic> joint is not its own independent coordinate: its# angle/displacement is master*multiplier+offset, so it maps onto# the master's own dof_idx everywhere below, chain-ruled by the# multiplier, rather than getting a column of its own.self.mimic_map={}forjlistinjoints_by_parent.values():forjinjlist:ifj["mimic_joint"]isnotNone:self.mimic_map[j["name"]]=(self.dof_index[j["mimic_joint"]],j["mimic_multiplier"],j["mimic_offset"])self.link_ancestor_dofs={root_link:[]}defcollect(link_name,ancestors):self.link_ancestor_dofs[link_name]=list(ancestors)forjinjoints_by_parent.get(link_name,[]):ifj["type"]=="fixed":next_ancestors=ancestorselifj["name"]inself.mimic_map:master_idx,mult,_offset=self.mimic_map[j["name"]]next_ancestors=ancestors+[(j,master_idx,mult)]else:next_ancestors=ancestors+[(j,self.dof_index[j["name"]],1.0)]collect(j["child"],next_ancestors)collect(root_link,[])self.link_names=list(links.keys())# Real per-joint limits from the URDF's own <limit> tags (±inf where# the URDF declares none, e.g. a "continuous" joint's position, or a# joint with no <limit> element at all -- never invented).self.q_min=jnp.array([j["q_min"]forjinself.dof_joints])self.q_max=jnp.array([j["q_max"]forjinself.dof_joints])self.qd_max=jnp.array([j["qd_max"]forjinself.dof_joints])
defforward_kinematics(self,q):"""Real link poses and joint axes in world frame, as a function of q. Returns ------- pos, rot, joint_axis_world : dict Keyed by link name (pos/rot) or joint name (joint_axis_world). """pos={self.root_link:jnp.zeros(3)}rot={self.root_link:jnp.eye(3)}joint_axis_world={}defwalk(link_name,p,r):forjinself._children_joints(link_name):r_offset=_rpy_to_matrix(jnp.asarray(j["rpy"]))p_child=p+r@jnp.asarray(j["xyz"])r_child=r@r_offsetaxis_world=r_child@(jnp.asarray(j["axis"])/jnp.linalg.norm(jnp.asarray(j["axis"])))ifj["type"]in("revolute","continuous"):joint_axis_world[j["name"]]=axis_worldangle=self._dof_value(j,q)r_child=r_child@_axis_angle_matrix(jnp.asarray(j["axis"]),angle)elifj["type"]=="prismatic":joint_axis_world[j["name"]]=axis_worlddisp=self._dof_value(j,q)p_child=p_child+axis_world*disppos[j["child"]]=p_childrot[j["child"]]=r_childwalk(j["child"],p_child,r_child)walk(self.root_link,jnp.zeros(3),jnp.eye(3))returnpos,rot,joint_axis_world
defcom_positions(self,q):"""World-frame center-of-mass position of every link, shape (n_links, 3)."""pos,rot,_=self.forward_kinematics(q)returnjnp.stack([pos[name]+rot[name]@jnp.asarray(self.links[name]["com"])fornameinself.link_names])
defpotential_energy(self,q):"""Real total gravitational potential energy at configuration q."""com=self.com_positions(q)masses=jnp.array([self.links[name]["mass"]fornameinself.link_names])returnjnp.sum(masses*com[:,2])*_G
deflink_position(self,q,link_name):"""Real world-frame origin position of the named link's own frame."""pos,rot,_=self.forward_kinematics(q)returnpos[link_name]
deflink_jacobian(self,q,link_name):"""Real translational Jacobian of the named link's own frame origin, shape (3, n)."""pos,rot,joint_axis_world=self.forward_kinematics(q)jv=jnp.zeros((3,self.n))forj,dof_idx,scaleinself.link_ancestor_dofs[link_name]:axis_w=joint_axis_world[j["name"]]ifj["type"]=="prismatic":jv=jv.at[:,dof_idx].add(scale*axis_w)else:p_joint=self._joint_origin_world(j,pos,rot)jv=jv.at[:,dof_idx].add(scale*jnp.cross(axis_w,pos[link_name]-p_joint))returnjv
deflink_pose(self,q,link_name):"""Real (position, rotation matrix) of the named link's own frame, in world frame."""pos,rot,_=self.forward_kinematics(q)returnpos[link_name],rot[link_name]
deflink_spatial_jacobian(self,q,link_name):"""Real 6xN spatial Jacobian [angular; linear] of the named link, in world frame."""pos,rot,joint_axis_world=self.forward_kinematics(q)p_link=pos[link_name]jv=jnp.zeros((3,self.n))jw=jnp.zeros((3,self.n))forj,dof_idx,scaleinself.link_ancestor_dofs[link_name]:axis_w=joint_axis_world[j["name"]]ifj["type"]=="prismatic":jv=jv.at[:,dof_idx].add(scale*axis_w)else:p_joint=self._joint_origin_world(j,pos,rot)jv=jv.at[:,dof_idx].add(scale*jnp.cross(axis_w,p_link-p_joint))jw=jw.at[:,dof_idx].add(scale*axis_w)returnjnp.concatenate([jw,jv],axis=0)
Promoted from Dense-Evolution-Discovery Experiment 67. Checked against a real central
finite difference of link_pose, not just plausibility: driving Franka Panda's
finger_joint1 (master) by 0.02 moves both fingertips by exactly 0.02 in opposite directions
(their local closing axes point opposite ways); the hand-built Jacobian's mimic column matches
the finite-difference derivative to under 1e-5.
Only one master per mimic, no transitive chains. A mimic joint's joint= attribute must
name an independent (non-mimic) joint; mimicking another mimic joint is not supported -- not
something real published URDFs do.