System Identification¶
System identification fits per-DOF dynamics parameters of an entity (damping, armature, stiffness,
frictionloss, and the PD gains kp / kd; masses are not fittable) so the
simulation reproduces recorded trajectories. A SystemIdentificationBase rolls candidate ParameterSets out in
parallel environments, scores them with a residual against Trajectory recordings, and runs an optimizer (CMA-ES or
SciPy least squares). Excitation signals drive the robot while you record, and DeploymentRecorder captures
real-robot trajectories during a deployment. The planning and sysid guide
walks through a fit.
from eden.extensions.sysid import CMAES, Parameter, ParameterSet, SYSID_REGISTRY, Trajectory
from eden.options.extensions.sysid import SystemIdentificationOptions
Base class, registry and result¶
Base class and fit-result type for system identification.
Attributes¶
Classes¶
FitResult
dataclass
¶
FitResult(cost: float, nfev: int, x: ndarray, jacobian: ndarray | None = None, message: str = '', history: ndarray | None = None, extras: dict[str, Any] = dict())
Structured diagnostics returned by :meth:SystemIdentificationBase.fit.
| ATTRIBUTE | DESCRIPTION |
|---|---|
cost |
Final 0.5 * ||residual||^2.
TYPE:
|
nfev |
Number of residual evaluations performed.
TYPE:
|
x |
Flat optimised parameter vector (matches
TYPE:
|
jacobian |
Jacobian at the optimum, when the backend provides one.
TYPE:
|
message |
Backend-specific termination message.
TYPE:
|
history |
Per-iteration cost history, when available.
TYPE:
|
extras |
Backend-specific additional diagnostics. |
SystemIdentificationBase
¶
SystemIdentificationBase(env: 'EnvBase', options: SystemIdentificationOptions)
Bases: ConfigurableMixin[SystemIdentificationOptions], ABC
Base class for system-identification backends.
Subclasses override :meth:fit to implement the optimisation loop.
The base class provides :meth:evaluate for one-shot residual evaluation
on the current parameter vector.
| PARAMETER | DESCRIPTION |
|---|---|
entity_name
|
Name of the entity whose parameters are being identified.
|
signals
|
Measurement signals included in the residual (
|
signal_weights
|
Per-signal weights (missing signals default to 1.0).
|
normalize
|
If True, normalise each signal block by its measured RMS.
|
max_iters
|
Maximum optimiser iterations.
|
verbose
|
Print per-iteration status.
|
Notes
The environment passed in should be a dedicated sysid env: a
standalone RLEnvBase built with env_options.num_envs equal to
the intended batched-candidate population (or 1 for serial backends).
The base class avoids RLEnvBase.step entirely so termination,
reward, reset, command, event, and recorder managers do not fire
during replay.
Source code in eden/extensions/sysid/base.py
Attributes¶
signals
class-attribute
instance-attribute
¶
Methods:¶
evaluate
¶
evaluate(params: ParameterSet, trajectories: Sequence[Trajectory]) -> ndarray
Rollout once per trajectory and return the flattened residual.
Source code in eden/extensions/sysid/base.py
fit
abstractmethod
¶
fit(params: ParameterSet, trajectories: Sequence[Trajectory]) -> tuple[ParameterSet, FitResult]
Run the optimisation loop and return the best ParameterSet + FitResult.
Source code in eden/extensions/sysid/base.py
Functions:¶
Options¶
Configuration options for system-identification extensions.
Attributes¶
DofProperty
module-attribute
¶
DofProperty = Literal['damping', 'armature', 'stiffness', 'frictionloss', 'kp', 'kd']
SignalName
module-attribute
¶
SignalName = Literal['dofs_pos', 'dofs_vel', 'dofs_torque', 'base_quat', 'base_ang_vel']
Classes¶
SystemIdentificationOptions
¶
Bases: DynamicConfigurableOptions
Options for sysid extensions.
| PARAMETER | DESCRIPTION |
|---|---|
entity_name
|
Name of the entity being identified (must exist in the scene).
|
signals
|
Subset of signals included in the residual. Defaults to proprioception.
|
signal_weights
|
Per-signal multiplicative weights. Missing signals default to 1.0.
|
normalize
|
Divide each signal block by its measured RMS before weighting.
|
max_iters
|
Maximum optimizer iterations / evaluations.
|
verbose
|
Log per-iteration progress.
|
Source code in eden/options/options.py
Attributes¶
signals
class-attribute
instance-attribute
¶
signals: Sequence[SignalName] = ('dofs_pos', 'dofs_vel', 'dofs_torque')
Parameters and trajectories¶
Parameter and parameter-set definitions for system identification.
Attributes¶
Classes¶
Parameter
¶
Parameter(name: str, property: DofProperty, dof_names: Sequence[str], entity_name: str = 'robot', per_dof: bool = False, nominal: float | Sequence[float] = 0.0, min_value: float | Sequence[float] = 0.0, max_value: float | Sequence[float] = 1.0, frozen: bool = False)
A single sysid parameter with nominal value, box bounds, and a DOF-property target.
A parameter drives one of the per-DOF solver fields
(damping, armature, stiffness, frictionloss) or one of
the per-DOF PD gains (kp, kd) on the entity named
entity_name for the listed dof_names.
If per_dof is True, value is a length-len(dof_names) vector.
Otherwise value is a length-1 vector that is broadcast to all listed
DOFs when applied to the entity.
Source code in eden/extensions/sysid/parameter.py
Attributes¶
min_value
instance-attribute
¶
max_value
instance-attribute
¶
Methods:¶
reset
¶
clip
¶
as_dof_vector
¶
as_dof_vector(n_dofs: int) -> ndarray
Broadcast value to a length-n_dofs vector for set_dofs_
Source code in eden/extensions/sysid/parameter.py
ParameterSet
¶
Ordered collection of Parameter objects with vectorised access.
Frozen parameters are skipped by as_vector / get_bounds /
update_from_vector so the decision-variable dimension seen by the
optimiser equals the number of free scalars.
Source code in eden/extensions/sysid/parameter.py
Attributes¶
Methods:¶
copy
¶
values
¶
as_vector
¶
as_nominal_vector
¶
update_from_vector
¶
Source code in eden/extensions/sysid/parameter.py
get_bounds
¶
get_bounds() -> tuple[ndarray, ndarray]
Source code in eden/extensions/sysid/parameter.py
reset
¶
sample
¶
save_yaml
¶
load_yaml
classmethod
¶
Source code in eden/extensions/sysid/parameter.py
Trajectory container for system identification.
Classes¶
Trajectory
dataclass
¶
Trajectory(times: ndarray, action: ndarray | None = None, dofs_pos: ndarray | None = None, dofs_vel: ndarray | None = None, dofs_torque: ndarray | None = None, base_quat: ndarray | None = None, base_ang_vel: ndarray | None = None, base_lin_acc: ndarray | None = None, dof_names: tuple[str, ...] = (), initial_state: dict[str, ndarray] = dict(), extra: dict[str, Any] = dict())
Measured-or-simulated trajectory for sysid.
All arrays are 2-D (n_steps, dim) (times is 1-D). Fields absent
from the measurement are stored as None and skipped by the residual.
| ATTRIBUTE | DESCRIPTION |
|---|---|
times |
Time stamps, shape
TYPE:
|
action |
Applied action / command, shape
TYPE:
|
dofs_pos, dofs_vel, dofs_torque |
Per-DOF measurements, shape
|
base_quat, base_ang_vel, base_lin_acc |
Floating-base signals in wxyz / body-frame IMU convention. Each
shape
|
dof_names |
Names in matching column order with dofs_*. |
initial_state |
Snapshot of the entity state at t=0 for rollout reset. Keys:
|
extra |
Free-form attachments (e.g. fingertip tactile, metadata). |
Attributes¶
initial_state
class-attribute
instance-attribute
¶
Methods:¶
save
¶
Source code in eden/extensions/sysid/trajectory.py
load
classmethod
¶
Source code in eden/extensions/sysid/trajectory.py
from_recorder_episode
classmethod
¶
from_recorder_episode(path: str | Path, dof_names: Sequence[str] | None = None, demo: str | None = None) -> 'Trajectory'
Load a Trajectory from a RecorderManager npz produced by SysIDRecorder.
Reads only the sysid/* key schema. NPZFileHandler writes one or
more episodes under demo_{n}/... keys; pass demo="demo_0" to
pick a specific episode, or leave it to auto-select the first one.
A flat npz whose keys are already under sysid/... (no demo_
prefix) is also supported.
Source code in eden/extensions/sysid/trajectory.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
Optimizers¶
CMA-ES optimizer for system identification.
Attributes¶
Classes¶
CMAES
¶
CMAES(env: 'EnvBase', options: SystemIdentificationOptions)
Bases: SystemIdentificationBase
CMA-ES identifier for noisy or contact-heavy residuals (e.g. dex hand).
By default evaluates candidates sequentially on the current env —
each candidate ask() writes its own parameter vector across all
envs and runs one replay per trajectory. This works with any env.num_envs and
matches the serial assumption used by SciPyLeastSquares.
To enable batched candidate evaluation set batched=True and
build the sysid env with env_options.num_envs == population_size;
a single replay then scores the whole population, reducing the
per-generation wall-clock by a factor close to the population size.
Source code in eden/extensions/sysid/base.py
Attributes¶
Methods:¶
fit
¶
fit(params: ParameterSet, trajectories: Sequence[Trajectory]) -> tuple[ParameterSet, FitResult]
Source code in eden/extensions/sysid/optimizers/cmaes.py
Functions:¶
SciPy least-squares optimizer for system identification.
Attributes¶
Classes¶
SciPyLeastSquares
¶
SciPyLeastSquares(env: 'EnvBase', options: SystemIdentificationOptions)
Bases: SystemIdentificationBase
Trust-Region Reflective least-squares with box bounds.
Operates on a unit-hypercube reparameterisation of the free parameters
— decision variables live in [0, 1] regardless of their physical
scale — so the finite-difference Jacobian uses a step of diff_step
fraction of each parameter's bound range (not sqrt(eps) of its
absolute value, which is below the sim's numerical floor for most
solver fields). This is the same reparameterisation the CMA-ES backend
uses.
Source code in eden/extensions/sysid/base.py
Attributes¶
Methods:¶
fit
¶
fit(params: ParameterSet, trajectories: Sequence[Trajectory]) -> tuple[ParameterSet, FitResult]
Source code in eden/extensions/sysid/optimizers/scipy_ls.py
Functions:¶
Excitation signals¶
Excitation signal generators (chirp, PRBS, playback).
Classes¶
Excitation
¶
Bases: ABC
Generator of per-DOF position commands used to excite the robot for sysid.
Implementations return a length-num_dofs array at each call. The
driver (DeploymentRecorder) adds this to its center pose (the robot's
pose at recording start, or center_dofs_pos) before sending to hardware.
ChirpExcitation
¶
ChirpExcitation(num_dofs: int, dof_indices: Sequence[int], f_start: float, f_end: float, duration: float, amplitude: float | Sequence[float], stagger_phase: float = 0.0)
Bases: Excitation
Linear-swept sine — the canonical sysid input for linear-system ID.
Produces amplitude * sin(2*pi * phase(t)) where the instantaneous
frequency ramps from f_start to f_end over duration. When
dof_indices lists multiple DOFs, each is phase-shifted by
stagger_phase * (idx / num_active) so simultaneous excitation
doesn't create degenerate rank-1 inputs.
Source code in eden/extensions/sysid/excitation.py
PRBSExcitation
¶
PRBSExcitation(num_dofs: int, dof_indices: Sequence[int], period: float, duration: float, amplitude: float | Sequence[float], seed: int = 0)
Bases: Excitation
Pseudo-random binary sequence — good for stiction / friction characterisation.
Every period seconds each DOF's command takes a fresh random sign,
+amplitude or -amplitude. The DOFs draw independent sequences
(from one seed), which avoids correlated excitation.
Source code in eden/extensions/sysid/excitation.py
PlaybackExcitation
¶
PlaybackExcitation(offsets: ndarray, dt: float)
Bases: Excitation
Replay a pre-recorded trajectory (e.g. captured earlier in sim).
offsets is (n_steps, num_dofs) of offsets from the center pose
sampled at dt. At arbitrary t the nearest earlier sample is
returned (zero-order hold) — robust to clock skew between generator
and deployment loop.
Source code in eden/extensions/sysid/excitation.py
Rollouts, residuals and applying parameters¶
Rollout helpers for replaying and evaluating system-id candidates.
Classes¶
Functions:¶
reset_to_initial_state
¶
reset_to_initial_state(env: 'EnvBase', trajectory: Trajectory, entity_name: str) -> None
Reset all envs to the measurement's initial state by writing entity state directly.
Bypasses _reset_idx on purpose: firing _RESET events would re-run
domain-randomisation terms that may mutate the very parameters being
identified. The caller is responsible for first placing the sysid
parameters via :func:apply_parameters / :func:apply_candidates.
For floating-base entities, qpos from a sim-side recording already
includes the 7-DOF base prefix (because :class:SysIDRecorder writes it
using entity.qs_idx_local). Deployment recordings, however, store
DOF positions only — pos and quat come through as separate
initial_state keys, and we apply them after set_qpos so the
base actually starts at the measured pose.
Source code in eden/extensions/sysid/rollout.py
replay_rollout
¶
replay_rollout(env: 'EnvBase', trajectory: Trajectory, entity_name: str, signals: Sequence[str] = ('dofs_pos', 'dofs_vel', 'dofs_torque')) -> dict[str, Tensor]
Replay the measured action trajectory under the currently-applied parameters.
Uses the narrow physics-only stepper so that termination, reward,
reset, command, event, and recorder managers do not fire — a measured
trajectory longer than max_episode_length would otherwise be
corrupted by auto-reset.
All env.num_envs envs step on every tick (Genesis steps the full
batch). For K-candidate batched identification, build a dedicated env
with env_options.num_envs = K and use :func:batched_candidate_rollout;
otherwise all envs receive the same state (but may hold different
parameters for Monte-Carlo variance estimation).
Returns a dict {signal_name: tensor(num_envs, n_steps, dim)}.
Source code in eden/extensions/sysid/rollout.py
single_candidate_rollout
¶
single_candidate_rollout(env: 'EnvBase', params: ParameterSet, trajectory: Trajectory, entity_name: str, signals: Sequence[str] = ('dofs_pos', 'dofs_vel', 'dofs_torque')) -> dict[str, ndarray]
Apply every free parameter's value to all envs, replay once, return env-0 predictions.
This is the serial primitive used by every optimiser that evaluates one parameter vector per step (scipy least-squares with FD Jacobian, and CMA-ES unless batched mode is opted into).
Source code in eden/extensions/sysid/rollout.py
batched_candidate_rollout
¶
batched_candidate_rollout(env: 'EnvBase', params: ParameterSet, candidates: ndarray, trajectory: Trajectory, entity_name: str, signals: Sequence[str] = ('dofs_pos', 'dofs_vel', 'dofs_torque')) -> dict[str, ndarray]
Evaluate K candidates in one rollout on a K-sized env.
Requires env.num_envs == K. Each candidate row is written into the
matching env via per-env DOF setters; a single replay then produces
predictions for all K candidates simultaneously.
Returns {signal_name: array(shape=(K, n_steps, dim))}.
Source code in eden/extensions/sysid/rollout.py
Signal-residual functions for system-identification objectives.
Classes¶
Functions:¶
signal_residual
¶
signal_residual(predicted: Mapping[str, ndarray], measured: Trajectory, signals: Sequence[str], weights: Mapping[str, float] | None = None, normalize: bool = True) -> ndarray
Flatten per-signal residuals into a single 1-D vector.
predicted[s] has shape (n_steps, dim). The measured value is read
from measured.signal(s); the two are compared over the shorter of
their lengths. A signal missing from either side is skipped. Each block is
multiplied by its weight and, when normalize=True, divided by its
measured RMS so heterogeneous units contribute comparably.
Source code in eden/extensions/sysid/residual.py
multi_trajectory_residual
¶
multi_trajectory_residual(predicted_trajectories: Sequence[Mapping[str, ndarray]], measured_trajectories: Sequence[Trajectory], signals: Sequence[str], weights: Mapping[str, float] | None = None, normalize: bool = True) -> ndarray
Concatenate residuals from multiple (predicted, measured) pairs.
Source code in eden/extensions/sysid/residual.py
Apply identified parameters / candidates to entities.
Attributes¶
DOF_PROPERTY_SETTERS
module-attribute
¶
DOF_PROPERTY_SETTERS: dict[str, str] = {'damping': 'set_dofs_damping', 'armature': 'set_dofs_armature', 'stiffness': 'set_dofs_stiffness', 'frictionloss': 'set_dofs_frictionloss', 'kp': 'set_dofs_kp', 'kd': 'set_dofs_kd'}
DOF_PROPERTY_GETTERS
module-attribute
¶
DOF_PROPERTY_GETTERS: dict[str, str] = {'damping': 'get_dofs_damping', 'armature': 'get_dofs_armature', 'stiffness': 'get_dofs_stiffness', 'frictionloss': 'get_dofs_frictionloss', 'kp': 'get_dofs_kp', 'kd': 'get_dofs_kd'}
Classes¶
Functions:¶
make_parameter_from_default
¶
make_parameter_from_default(env: 'EnvBase', name: str, property: str, dof_names: Sequence[str], entity_name: str = 'robot', min_scale: float = 0.1, max_scale: float = 10.0, per_dof: bool = False, frozen: bool = False) -> Parameter
Construct a Parameter using the entity's current value as the nominal.
Reads the current per-DOF value (env 0) via the entity getter and uses it
as nominal — per DOF with per_dof, else their mean. Bounds are
min_scale / max_scale times nominal, the natural parameterisation
for the supported non-negative properties (damping, armature,
stiffness, frictionloss, kp, kd). When the default is zero
(e.g. frictionloss), the multiplicative bounds collapse — construct the
Parameter explicitly in that case.
Source code in eden/extensions/sysid/modifier.py
apply_parameters
¶
apply_parameters(env: 'EnvBase', params: ParameterSet) -> None
Write every free parameter's current value to all envs.
Broadcasts the parameter vector to (num_envs, n_dofs) so the
solver tensors are consistent across the batch.
Source code in eden/extensions/sysid/modifier.py
apply_candidates
¶
apply_candidates(env: 'EnvBase', params: ParameterSet, candidates: ndarray) -> None
Write K distinct candidate parameter vectors into K environments.
candidates has shape (K, n_free_params) and requires
K == env.num_envs. Each row is written to env k as the per-env
value of the targeted solver tensor. Frozen parameters are untouched.
Source code in eden/extensions/sysid/modifier.py
Recording on the robot¶
Recorder that captures real-robot trajectories for system identification.
Classes¶
DeploymentRecorder
¶
DeploymentRecorder(deployer: 'DeploymentBase', excitation: Excitation, kp_scale: float = 1.0, kd_scale: float = 1.0, safety_limits: ndarray | None = None, center_dofs_pos: ndarray | None = None, dofs_limit: tuple[ndarray, ndarray] | None = None)
Drive a DeploymentBase with an Excitation and record a Trajectory.
The loop writes the excitation offset on top of a center pose at
every control tick, captures the returned RobotState, and after the
duration serialises everything to a Trajectory compatible with the
sysid rollout loader.
| PARAMETER | DESCRIPTION |
|---|---|
deployer
|
Active deployment backend, connected and already at its start pose (an INIT that has finished:
TYPE:
|
excitation
|
Excitation generator. Called once per control tick with the elapsed
seconds since
TYPE:
|
kp_scale
|
Multipliers on
TYPE:
|
kd_scale
|
Multipliers on
TYPE:
|
safety_limits
|
Optional per-DOF symmetric clamp on the excitation offset (radians). Defaults to no additional clamp beyond whatever the backend already enforces.
TYPE:
|
center_dofs_pos
|
Optional absolute joint-position center for the excitation. Defaults to the robot's actual position at recording start.
TYPE:
|
dofs_limit
|
Optional per-DOF lower and upper position limits used for diagnostics.
TYPE:
|
Source code in eden/extensions/sysid/deployment_recorder.py
Attributes¶
safety_limits
instance-attribute
¶
center_dofs_pos
instance-attribute
¶
center_dofs_pos = np.asarray(center_dofs_pos, dtype=np.float64) if center_dofs_pos is not None else None
Methods:¶
run
¶
run() -> Trajectory
Execute the excitation for excitation.duration seconds.
By default, the excitation offset is added on top of the robot's
actual resting pose (read_state().dofs_pos). Callers can pass
center_dofs_pos to use a known center pose instead, such as the
midpoint of each joint's position limits.
Returns the recorded Trajectory. The timestamp column starts at zero (relative to loop start); the caller can shift externally if absolute alignment with other logs is needed.
Source code in eden/extensions/sysid/deployment_recorder.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
Reporting¶
Reporting helpers for system identification (tables, plots, summary).
Classes¶
Functions:¶
parameter_table
¶
parameter_table(initial: ParameterSet, identified: ParameterSet, digits: int = 4) -> str
Human-readable comparison of initial vs identified parameters.
Source code in eden/extensions/sysid/report.py
signal_plots
¶
signal_plots(predicted: Mapping[str, ndarray] | Mapping[str, Mapping[str, ndarray]], measured: Trajectory, signals: Sequence[str], save_dir: str | Path, dof_names: Sequence[str] | None = None) -> list[Path]
Write per-signal matplotlib plots comparing one or more predictions against measurement.
predicted accepts two forms:
- Single trace: {signal_name: array(n_steps, dim)} — one
dashed "predicted" line is plotted alongside the measurement.
- Multiple named traces: {label: {signal_name: array(...)}}
— each label is drawn with its own line style, e.g.
{"initial": pred0, "identified": pred1} to visualise the
pre- vs post-optimisation fit on the same axes.
Each signal gets one figure with one subplot per DOF column. Requires matplotlib; returns the list of written file paths.
Source code in eden/extensions/sysid/report.py
write_summary
¶
write_summary(save_dir: str | Path, initial: ParameterSet, identified: ParameterSet, result: 'FitResult') -> None
Write identified parameters as YAML plus a human-readable summary.txt.