Source code for urdf_state_space.config
"""YAML-driven model setup: describe the linearization per joint, by name.
Example config
--------------
::
model:
urdf: package://my_robot_description/urdf/my_robot.urdf
floating_base: false
joints: # every movable joint may have an entry
shoulder_joint: {actuated: true, damping: 0.1}
elbow_joint: {actuated: true, damping: 0.1, q_eq: 0.3}
wrist_joint: {damping: 0.005}
outputs:
velocities: true # y includes velocities as well
# joints: [elbow_joint] # optional; default: the actuated joints
export:
npz: model.npz
mat: model.mat
# dt: 0.001 # also produce/export the ZOH-discretized model
If any joint sets ``actuated``, only joints with ``actuated: true`` are
actuated; if none does, all movable joints are. Joints without a ``damping``
or ``q_eq`` entry default to 0 / neutral. Relative paths (URDF and export
files) are resolved against the YAML file's directory.
"""
import os
import subprocess
from dataclasses import dataclass
from typing import Optional, Tuple
import yaml
from .state_space import StateSpaceModel, build_state_space
[docs]
@dataclass
class ExportSpec:
npz: Optional[str] = None
mat: Optional[str] = None
dt: Optional[float] = None
[docs]
def resolve_resource(ref: str, base_dir: str = '.') -> str:
"""Resolve a package:// URI or a (possibly relative) filesystem path."""
if ref.startswith('package://'):
from ament_index_python.packages import get_package_share_directory
pkg, _, rel = ref[len('package://'):].partition('/')
return os.path.join(get_package_share_directory(pkg), rel)
if os.path.isabs(ref):
return ref
return os.path.join(base_dir, ref)
def _read_urdf(path: str) -> str:
if path.endswith('.xacro'):
return subprocess.check_output(['xacro', path], text=True)
with open(path) as f:
return f.read()
[docs]
def build_from_yaml(config_path: str) -> Tuple[StateSpaceModel, ExportSpec]:
"""Build a StateSpaceModel from a YAML config; also return export spec."""
with open(config_path) as f:
cfg = yaml.safe_load(f) or {}
base_dir = os.path.dirname(os.path.abspath(config_path))
model_cfg = cfg.get('model', {})
if 'urdf' not in model_cfg:
raise ValueError(f'{config_path}: missing required key model.urdf')
urdf_path = resolve_resource(str(model_cfg['urdf']), base_dir)
joints = cfg.get('joints') or {}
for name, entry in joints.items():
if not isinstance(entry, dict):
raise ValueError(
f'{config_path}: joints.{name} must be a mapping, '
f'e.g. {{actuated: true, damping: 0.1}}')
q_eq = {n: e['q_eq'] for n, e in joints.items() if 'q_eq' in e}
damping = {n: e['damping'] for n, e in joints.items() if 'damping' in e}
actuated = None
if any('actuated' in e for e in joints.values()):
actuated = [n for n, e in joints.items() if e.get('actuated')]
if not actuated:
raise ValueError(f'{config_path}: no joint has actuated: true')
out_cfg = cfg.get('outputs') or {}
output_joints = out_cfg.get('joints')
model = build_state_space(
_read_urdf(urdf_path),
q_eq=q_eq or None,
actuated_joints=actuated,
output_joints=output_joints,
velocity_outputs=bool(out_cfg.get('velocities', False)),
joint_damping=damping or None,
floating_base=bool(model_cfg.get('floating_base', False)),
)
exp = cfg.get('export') or {}
export = ExportSpec(
npz=resolve_resource(exp['npz'], base_dir) if 'npz' in exp else None,
mat=resolve_resource(exp['mat'], base_dir) if 'mat' in exp else None,
dt=float(exp['dt']) if 'dt' in exp else None,
)
return model, export