Real robot descriptions are rarely shipped as a single flat URDF. Manufacturers publish them as
.xacro macro files: parametrized building blocks (xacro:macro), math expressions
(${-pi/2}), conditionals (xacro:unless) and includes (xacro:include) that get expanded into
a plain URDF before anything can parse them. RigidBodyModel only accepted the expanded form.
The real xacro package (the same expander the ROS ecosystem itself uses, no ROS install
required) does the expansion; nothing about macros, math, or conditionals is reimplemented here.
A .xacro extension routes through xacro.process_file(path).toxml() first; any other
extension parses as a plain URDF exactly as before.
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 66. Expanding the Franka Panda's own
published macros (panda_arm.xacro + hand.xacro, from clvrai/furniture) first produced a
7-joint model and a KeyError: 'panda_hand' -- the hand macro attaches with
connected_to="panda_link8", but the arm macro's own panda_link8/panda_joint8 block was
commented out in the source. The separately checked-in, pre-expanded panda_arm_hand.urdf in
the same upstream repo does include that link, meaning it was generated from an earlier,
uncommented version of the same macro. Restoring that block (same real values: mass 0.005,
inertia 0.00003, origin 0 0 0.107) reconnects the tree.