Planning and system identification¶
Three extensions sit beside the RL stack: motion planners that compute collision-free joint paths for an arm (cuRobo, OMPL), offline motion optimization that adapts a recorded clip to terrain (TCRS), and system identification that fits a robot's joint dynamics in simulation to measurements from the real robot. This page covers what each one does, how to call it, and its limits.
Motion planning¶
A motion planner takes a built env and the name of an entity, and returns joint-space waypoints that you feed to a
joint-position action term. Both planners subclass MotionPlannerBase (eden/extensions/motion_planning/base.py),
register in MOTION_PLANNER_REGISTRY, and are configured through PlannerOptions (entity_name, default
"robot"; backend fields arrive as extras).
CuroboPlanner |
OMPLPlanner |
|
|---|---|---|
| Module | eden.extensions.motion_planning.curobo |
eden.extensions.motion_planning.ompl |
| Goal | end-effector pose (ee_goal_pos, ee_goal_quat, wxyz) |
joint configuration (qpos_goal) |
| Batched over envs | yes | no (one env per call, envs_idx) |
| Collision world | meshes of the entities in collision_entities_name |
the entity's own contacts in the live scene (detect_collision) |
| Install | cuRobo (see cuRobo's install guide); no Eden extra | pip install ompl (OMPL 1.7 or 2.x; see OMPL releases); not available on Windows; no Eden extra |
| Recipe | examples/recipes/planning/path_planning_curobo.py |
examples/recipes/planning/path_planning_ompl.py |
Both raise an ImportError with the install pointer when their library is missing.
cuRobo¶
# excerpt from examples/recipes/planning/path_planning_curobo.py
import torch
from curobo.util_file import get_robot_configs_path, join_path
from eden.extensions.motion_planning.curobo import CuroboPlanner
from eden.options.extensions.motion_planning import PlannerOptions
planner = CuroboPlanner(
env,
PlannerOptions(
entity_name="robot",
robot_config_file=join_path(get_robot_configs_path(), "franka.yml"), # cuRobo's own Franka config
ee_link_name="hand",
collision_entities_name=["table", "plate"],
),
)
target_pos = env.entities["plate"].get_pos()
target_pos[:, 2] += 0.2
waypoints = planner.plan_motion(
ee_goal_pos=target_pos,
ee_goal_quat=torch.tensor([[0, 1, 0, 0]] * env.num_envs).to(env.device),
)
# waypoints: one (num_waypoints, n_dof) tensor per env
CuroboPlanner fields: robot_config_file (a cuRobo robot YAML; its cspace joints minus lock_joints are the
planned DOFs), ee_link_name, collision_entities_name (default none) and interpolation_dt (default 0.05 s).
- The collision world is built on the first
plan_motioncall. When the scene has changed since, callplanner.update_world(carried_entities=[...], num_envs=..., envs_idx=...)first; entities incarried_entities(an object in the gripper) are left out of the obstacles. - The goal pose is in the world frame; the planner converts it into the robot's base frame.
- Per env, a failed plan returns the start configuration as its single waypoint. Different envs return different waypoint counts, so the recipe pads the shorter paths while stepping.
OMPL¶
# excerpt from examples/recipes/planning/path_planning_ompl.py
from eden.extensions.motion_planning.ompl import OMPLPlanner
from eden.options.extensions.motion_planning import PlannerOptions
# robot = env.entities["robot"]; target_pos (num_envs, 3) and target_quat (num_envs, 4, wxyz) are the hand's goal
planner = OMPLPlanner(env, PlannerOptions(entity_name="robot"))
qpos_goal = robot.inverse_kinematics(links_name="hand", poss=target_pos, quats=target_quat)[0]
waypoints = planner.plan_motion(qpos_goal=qpos_goal, envs_idx=0)
for waypoint in waypoints: # each an (n_qs,) tensor
env.step(waypoint[:7].unsqueeze(0).repeat(env.num_envs, 1))
plan_motion(qpos_goal, qpos_start=None, resolution=0.01, timeout=5.0, max_retry=1, smooth_path=True,
num_waypoints=100, ignore_collision=False, planner="RRTConnect", envs_idx=0). planner is the name of any
ompl.geometric planner class.
- The return shape differs from cuRobo's: a list of waypoints for the one env, each an
(n_qs,)tensor, not a list per env. An infeasible problem returns an empty list (with a warning); one that does not converge withinmax_retryattempts returns the approximate path, or an empty list. - Collision checking moves the entity: each sampled state is written with
set_qposand checked withdetect_collision. Contacts already present at the start configuration (a gripper welded to its flange, an object resting in the hand) are ignored. The entity is put back atqpos_startwhen planning ends. - Start or goal outside the joint limits relaxes the limits to contain them, with a warning.
Motion optimization (TCRS)¶
eden.extensions.motion_optimization.tcrs is Eden's port of the TCRS terrain-adaptive motion optimizer: it turns
a flat-ground biped clip into one that walks on a terrain height field (footstep planning with MPPI, support-aware
pelvis height, whole-body Jacobian IK on a MuJoCo model of the robot). It is env-free and offline. Use it directly
through TCRSMotionAdapter, or on a whole motion dataset through the TCRSTerrainAdapt dataset modifier.
TCRS terrain adaptation is the full guide; examples/recipes/motion_optimization/tcrs/
holds the runnable recipes (adapt_clip.py, adapt_dataset.py, render_on_terrain.py).
System identification¶
eden.extensions.sysid fits per-DOF dynamics parameters of an Eden robot — damping, armature, stiffness,
frictionloss, and the PD gains kp / kd — so that the simulated response to a recorded action trace matches
the measured one. The workflow:
flowchart LR
A["excite the robot<br/>(DeploymentRecorder or SysIDRecorder)"] --> B["Trajectory (.npz)"]
B --> C["fit a ParameterSet<br/>(SciPyLeastSquares or CMAES)"]
C --> D["params_identified.yaml"]
D --> E["verify on a held-out trajectory"]
examples/projects/sysid/xhand1/ is the complete worked example for a real XHand1 (collect, identify, verify),
and its README.md is the step-by-step guide, including the troubleshooting list.
1. Record a trajectory¶
A Trajectory (eden/extensions/sysid/trajectory.py) holds times and (n_steps, dim) arrays: action (the
action the action manager received, which the replay re-injects), dofs_pos, dofs_vel, dofs_torque,
base_quat, base_ang_vel, base_lin_acc, plus dof_names and an initial_state (qpos, dofs_vel, and
optionally pos, quat). Fields the measurement lacks are None and skipped. save(path) / load(path) use NPZ.
From a real robot, drive a connected eden deploy backend with an excitation:
from eden.extensions.sysid import ChirpExcitation, DeploymentRecorder
# deployer: a connected DeploymentBase at its start pose; safety_limits: (num_dofs,) offset clamp in rad
excitation = ChirpExcitation(
num_dofs=deployer.num_dofs, dof_indices=range(deployer.num_dofs),
f_start=0.2, f_end=3.0, duration=30.0, amplitude=0.2,
)
recorder = DeploymentRecorder(deployer, excitation, kp_scale=0.5, safety_limits=safety_limits)
trajectory = recorder.run()
trajectory.save("data/chirp.npz")
deployermust be connected and already at its start pose (its INIT finished). The recorder writes the wire directly, outside the deployment's operator state machine.- The excitation is an offset added to a centre pose — the robot's measured pose at the start, or
center_dofs_pos.safety_limitsclamps the offset per DOF (radians), and the recorder warns, per DOF, when the centre ± peak offset leaves the joint limits: a joint driven into its stop clips on the real robot, and the fit then explains the clipping with friction. - Excitations:
ChirpExcitation(a swept sine,stagger_phaseto decorrelate DOFs),PRBSExcitation(a pseudo-random binary sequence; good for stiction and friction) andPlaybackExcitation(replay recorded offsets with a zero-order hold).
In simulation (for a sim-to-sim check, or to test the pipeline on known parameters), add SysIDRecorder to the
env's recorder options and load an episode back with Trajectory.from_recorder_episode(path, dof_names=...,
demo="demo_0"); it reads the sysid/* keys of the recorder's default NPZ file. SysIDRecorder(entity_name="robot",
include_torque=True, include_base=True) records the action, a timestamp, joint state, the control force and the
base orientation and angular velocity; set include_base=False for a fixed-base arm. See
Datasets and recording.
2. Build the sim twin¶
Fit on a dedicated sysid env: an RLEnvBase holding the robot, built (env.build()), with no task logic it
needs. The rollouts call the action manager and scene.step() directly (replay_rollout), so terminations,
rewards, resets, commands, events and recorders never fire: a trajectory longer than the episode is not cut by an
auto-reset, and a domain-randomization event cannot change the parameters being fitted. Each rollout starts by
writing initial_state into the entity.
- Match the cadence:
sim_dt * decimationmust equal the trajectory'sdt, or the replay runs at another rate than the robot did. The XHand1 example warns above a 2 % mismatch. - Use a torque-mode controller such as
ExplicitPDController(as the XHand1 sim twin does), so the PD damping is computed explicitly and the fittedkdandarmaturemean what they say. UnderEnvOptions(integrator="euler")a position-mode controller'skd · substep_dtis folded into the armature at build — see Entities. num_envs:1for the serial optimizers, the population size for batched CMA-ES,n_params + 1forparallel_fdleast-squares.
3. Fit the parameters¶
from eden.extensions.sysid import ParameterSet, SciPyLeastSquares, Trajectory, make_parameter_from_default
from eden.extensions.sysid.report import write_summary
from eden.options.extensions.sysid import SystemIdentificationOptions
# env: the built sim twin (step 2); dof_names: the joints to fit, in the trajectory's column order
trajectories = [Trajectory.load("data/chirp.npz"), Trajectory.load("data/prbs.npz")]
params = ParameterSet([
make_parameter_from_default(env, prop, prop, dof_names, per_dof=True) # name, property, DOFs
for prop in ("damping", "armature", "kp", "kd")
])
sysid = SciPyLeastSquares(
env=env,
options=SystemIdentificationOptions(entity_name="robot", signals=("dofs_pos",), max_iters=200, diff_step=1e-2),
)
initial = params.copy()
identified, result = sysid.fit(params, trajectories)
write_summary("results/sysid", initial, identified, result) # params_identified.yaml, params_initial.yaml, summary.txt
- Parameters. A
Parameter(name, property, dof_names, entity_name="robot", per_dof=False, nominal, min_value, max_value, frozen=False)targets one property on a set of DOFs, one scalar shared by all of them or one value per DOF.make_parameter_from_default(env, name, property, dof_names, min_scale=0.1, max_scale=10.0, ...)takes the built entity's current value as the nominal and bounds it multiplicatively; when that value is zero (an asset with nofrictionloss), the bounds collapse, so construct thatParameterexplicitly. Frozen parameters are kept but not optimized.ParameterSet.save_yaml/load_yamlround-trip a set. - Residual.
SystemIdentificationOptions:entity_name,signals(any ofdofs_pos,dofs_vel,dofs_torque,base_quat,base_ang_vel; default the first three),signal_weights,normalize(defaultTrue: each signal block is divided by its measured RMS so units mix fairly),max_iters(200),verbose. Fit only the signals the robot really measures: a velocity finite-differenced from positions, or a torque in firmware units, biases the fit. Several trajectories are fitted jointly, which improves conditioning (a chirp and a PRBS trace have complementary spectra). - Optimizers (both work in a unit hypercube over the parameter bounds):
SciPyLeastSquares— trust-region reflective least squares with box bounds.diff_step(default1e-2, a fraction of each bound range) is the finite-difference step: too small and the Jacobian is numerical noise, so the optimizer stops at iteration 0.parallel_fd=Truescores all Jacobian columns in one batched replay and needsenv.num_envs == n_params + 1exactly (any other count raises).CMAES— for noisy, contact-heavy or saddle-shaped residuals. Needs thecmapackage (pip install cma; no Eden extra declares it).sigma0(default0.25),population_size(default CMA's own), andbatched=Trueto score the whole population in one replay on an env withnum_envs == population_size. When warm-starting from a previous fit, lowersigma0(the XHand1 example uses0.05) or the first population scatters away from it.
fit()returns the identifiedParameterSetand aFitResult(cost,nfev,x,jacobian,message,history,extras).sysid.evaluate(params, trajectories)returns the residual of one parameter set without optimizing.
4. Apply and verify¶
apply_parameters(env, params) writes a parameter set into every env (through set_dofs_damping,
set_dofs_armature, set_dofs_stiffness, set_dofs_frictionloss, set_dofs_kp, set_dofs_kd);
apply_candidates(env, params, candidates) writes one candidate per env. To verify, replay a held-out trajectory
with single_candidate_rollout(env, params, trajectory, entity_name) under the nominal and the identified set and
compare the error. eden.extensions.sysid.report.signal_plots(...) overlays measured and predicted signals.
Carry the identified values into a task through its robot's default_dofs_* fields.
Adding an optimizer¶
Subclass SystemIdentificationBase, implement fit(params, trajectories) -> (ParameterSet, FitResult) with the
rollout helpers (single_candidate_rollout, batched_candidate_rollout) and multi_trajectory_residual, declare
backend options as class attributes (they arrive as SystemIdentificationOptions extras), and register it with
@SYSID_REGISTRY.register().
See also¶
- Grasp sampling — grasp poses to plan towards.
- TCRS terrain adaptation — the motion-optimization guide.
- Deployment — the backends
DeploymentRecorderdrives. - Entities — armature, damping and the two DOF index spaces.
- Datasets and recording —
SysIDRecorderand recorder files.