Kinova.Arm.Example.MOV
A generic robotic arm system identification tool that identifies the inertial parameters and friction of a robot's terminal (end-effector) link from MuJoCo simulation data.
This tool automates the process of identifying the 10 standard rigid-body inertial parameters (mass, center-of-mass moments, and inertia tensor) plus optional Coulomb and viscous friction coefficients for the last link of a robotic arm. It works with any robot described by a MuJoCo XML model.
Common use case: You've attached a payload or tool to your robot's end-effector and need to identify the composite inertial properties of that terminal link plus payload for accurate dynamic modeling and control.
The tool implements a physics-based system identification pipeline:
- Parse — Load a MuJoCo XML model and extract the kinematic chain, identifying the terminal (end-effector) joint automatically
- Generate — Create multi-frequency sinusoidal excitation trajectories that richly excite the terminal link's dynamics while respecting joint limits
- Simulate — Run MuJoCo simulation along the trajectory, collecting spatial velocity, acceleration, and torque data at each timestep
-
Solve — Compute the regressor matrix Y from spatial dynamics such that
$\tau = Y \theta$ , then solve for parameters via least-squares:$\theta = Y^{\dagger} \tau$ - Export — Output identified parameters as JSON, generate an updated MuJoCo XML with corrected inertias, and optionally export a trajectory CSV for hardware playback on Damiao motors
The spatial dynamics regressor Y is derived symbolically via SymPy, ensuring analytical correctness. The regressor relates joint torques to the 10 inertial parameters (mass, first moment of mass, inertia tensor) plus 2 friction coefficients (Coulomb and viscous).
- Robot-agnostic — Works with any MuJoCo XML model, not limited to specific robot types
- Automatic terminal joint detection — Identifies the end-effector joint automatically from the kinematic tree
- Analytical derivatives — Trajectory velocities and accelerations are computed exactly, not via numerical differentiation
- Symbolic regressor derivation — Uses SymPy to derive the spatial dynamics regressor, ensuring mathematical correctness
- Friction identification — Optionally identifies Coulomb and viscous friction coefficients for the terminal joint
- Hardware export — Generates trajectory files for Damiao motors with automatic limit clamping
- Quality metrics — Reports RMSE, condition number, and matrix rank to assess identification quality
- Python 3.9 or later
- MuJoCo 3.0 or later
# Clone the repository
git clone https://github.com/curryabalone/Robotic-Arm-System-ID-.git
cd Robotic-Arm-System-ID-
# Install in editable mode
pip install -e .This installs the robot-sysid command-line tool and all required dependencies (numpy, mujoco, sympy, scipy).
# Clone the repository
git clone https://github.com/curryabalone/Robotic-Arm-System-ID-.git
cd Robotic-Arm-System-ID-
# Create and activate conda environment
conda env create -f environment.yml
conda activate robot-sysid
# Install the package
pip install -e .For running tests and contributing to development:
pip install -e ".[dev]"This adds pytest and hypothesis for unit and property-based testing.
Run system identification on the included Kinova arm example:
robot-sysid examples/kinova/model/kinova_fullinertia_guess.xmlWhat happens:
- Loads the Kinova 7-DOF arm model with guessed inertial parameters
- Generates a 15-second multi-frequency sinusoidal excitation trajectory for the terminal joint (joint6)
- Runs MuJoCo simulation to collect regressor matrix Y and torque vector τ
- Solves for the 10 inertial parameters + 2 friction coefficients via least-squares
- Saves results to
./sysid_output/:params.json— Identified parameters in JSON formatupdated_model.xml— MuJoCo XML with corrected inertial parameters
Expected output:
Loading robot model from examples/kinova/model/kinova_fullinertia_guess.xml...
Identified terminal joint: joint6
Generating excitation trajectory (15.0s, 1000 Hz)...
Running MuJoCo simulation...
Collecting regressor data: 100%
Solving for parameters...
Identified parameters:
Mass: 0.789 kg
CoM moment: [-0.000, 0.009, 0.120] kg·m
Inertia: [0.024, 0.000, 0.000, 0.024, -0.002, 0.001] kg·m²
Coulomb friction: 0.40 N·m
Viscous friction: 0.10 N·m·s/rad
RMSE: 0.0001 N·m
Condition number: 1234.5
Base parameters: 7
Results saved to ./sysid_output/
robot-sysid <path-to-mujoco-xml> [options]| Option | Default | Description |
|---|---|---|
--mesh-dir MESH_DIR |
auto | Path to mesh directory (inferred from XML meshdir attribute if not specified) |
--duration DURATION |
15 | Trajectory duration in seconds (longer = better conditioning) |
--sample-rate RATE |
1000 | Sample rate in Hz (higher = more data points) |
--no-friction |
— | Disable friction identification (identify only 10 inertial params) |
--output-dir DIR |
./sysid_output |
Output directory for results |
--export-damiao |
— | Export trajectory CSV for Damiao motor playback |
--motor-type TYPE |
— | Damiao motor type per joint (e.g., DM4340). Repeat for each joint when using --export-damiao |
--seed SEED |
— | Random seed for reproducible trajectory generation |
robot-sysid examples/kinova/model/kinova_fullinertia_guess.xml --no-frictionSolves for 10 parameters: mass, first moment of mass (3), inertia tensor (6).
robot-sysid examples/kinova/model/kinova_fullinertia_guess.xml --duration 30 --sample-rate 500Uses a 30-second trajectory sampled at 500 Hz. Longer trajectories improve regressor conditioning but take more time to simulate.
robot-sysid examples/kinova/model/kinova_fullinertia_guess.xml --output-dir results/kinova_sysidSaves results to results/kinova_sysid/ instead of the default ./sysid_output/.
robot-sysid examples/kinova/model/kinova_fullinertia_guess.xml \
--export-damiao \
--motor-type DM4340 \
--motor-type DM4340 \
--motor-type DM4340 \
--motor-type DM4340 \
--motor-type DM4340 \
--motor-type DM4340 \
--motor-type DM4340Generates damiao_trajectory.csv with timestamped position, velocity, and feedforward torque commands for each joint, clamped to the specified motor limits. Useful for running the same trajectory on real hardware.
Supported Damiao motor types: DM4310, DM4340, DM4340_48V, DM6006, DM8006, DM10010L, DM8009
robot-sysid examples/kinova/model/kinova_fullinertia_guess.xml --seed 42Uses a fixed random seed for deterministic trajectory generation, ensuring reproducible results across runs.
JSON file containing the identified parameters and quality metrics:
{
"joint_name": "joint6",
"mass": 0.789160,
"center_of_mass_moment": [-0.000047, 0.009093, 0.120437],
"inertia_tensor": {
"Ixx": 0.024382,
"Ixy": 0.000001,
"Ixz": 0.000009,
"Iyy": 0.024312,
"Iyz": -0.001638,
"Izz": 0.000551
},
"coulomb_friction": 0.4,
"viscous_friction": 0.1,
"rmse": 0.000123,
"condition_number": 1234.56,
"n_base_params": 7
}Parameter definitions:
- mass — Total mass of the terminal link (kg)
- center_of_mass_moment — First moment of mass [hx, hy, hz] = m × [cx, cy, cz] (kg·m)
- inertia_tensor — 6 independent components [Ixx, Ixy, Ixz, Iyy, Iyz, Izz] about the body frame origin (kg·m²)
- coulomb_friction — Constant friction torque (N·m)
- viscous_friction — Velocity-dependent friction coefficient (N·m·s/rad)
Quality metrics:
- rmse — Root mean square error between measured and predicted torques (N·m). Lower is better. Typical values: < 0.01 N·m for simulation data.
- condition_number — Measure of regressor matrix conditioning. Lower is better. Values > 1000 may indicate poor excitation.
- n_base_params — Number of identifiable parameter combinations (matrix rank). Should be close to 10 (or 12 with friction) for well-conditioned data.
A copy of the input MuJoCo XML with the terminal body's inertial parameters replaced by the identified values. All other elements (meshes, actuators, sensors, visual elements) are preserved.
The updated XML includes a comment noting which body was identified and the identification RMSE:
<!-- System ID: body 'link6' identified with RMSE 0.000123 Nm -->
<body name="link6">
<inertial pos="0.0 0.0115 0.1526" mass="0.789160"
fullinertia="0.024382 0.024312 0.000551 0.000001 0.000009 -0.001638"/>
...
</body>Generated when --export-damiao is specified. Contains timestamped rows with position, velocity, and feedforward torque for each joint:
# motor_type=DM4340,can_id=1,mode=MIT
# motor_type=DM4340,can_id=2,mode=MIT
# motor_type=DM4340,can_id=3,mode=MIT
# ...
time,j0_pos,j0_vel,j0_tau,j1_pos,j1_vel,j1_tau,...
0.000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,...
0.001,0.0012,0.3100,0.0500,0.0008,0.2200,0.0300,...
0.002,0.0024,0.3150,0.0510,0.0016,0.2250,0.0305,...Values are automatically clamped to the specified Damiao motor limits. The tool reports the percentage of clamped points per joint.
The tool uses the spatial dynamics formulation to derive a linear relationship between joint torques and inertial parameters:
Where:
-
$\tau$ — Joint torque (scalar for a single joint) -
$Y$ — Regressor matrix (n_samples × 10 without friction, or n_samples × 12 with friction) -
$\theta$ — Parameter vector:$[m, h_x, h_y, h_z, I_{xx}, I_{xy}, I_{xz}, I_{yy}, I_{yz}, I_{zz}]$ for inertial-only, or$[m, h_x, h_y, h_z, I_{xx}, I_{xy}, I_{xz}, I_{yy}, I_{yz}, I_{zz}, f_c, f_v]$ with friction
The regressor Y is derived from the spatial dynamics equation:
Where:
-
$\mathbf{F}_a$ — 6D wrench (force + torque).
-
$\mathbf{G}_a$ — Spatial inertia matrix (function of$\theta$ ) -
$\mathbf{V}_a$ — Spatial velocity (angular + linear).
-
$\dot{\mathbf{V}}_a$ — Spatial acceleration (angular + linear).
-
$[\text{ad}_{\mathbf{V}_a}]$ — Adjoint operator (captures gyroscopic effects)
The 6D wrench is projected onto the joint axis to obtain the scalar joint torque. The regressor is derived symbolically using SymPy and cached for efficient evaluation.
The spatial inertia matrix
where:
-
$\bar{\mathbf{I}}$ — inertia tensor.
-
$\mathbf{h}$ — first moment of mass (mass times center of mass).
-
$[\mathbf{h}]_\times$ — skew-symmetric matrix.
The adjoint transpose
where:
and
The full
where each element is:
The complete regressor matrix with all 60 terms is:
where:
- Row 1: Torque component about x-axis (
$n_x$ ) - Row 2: Torque component about y-axis (
$n_y$ ) - Row 3: Torque component about z-axis (
$n_z$ ) - Row 4: Force component along x-axis (
$f_x$ ) - Row 5: Force component along y-axis (
$f_y$ ) - Row 6: Force component along z-axis (
$f_z$ )
And columns correspond to parameters:
- Column 1: Mass
$m$ - Column 2: First moment
$h_x$ - Column 3: First moment
$h_y$ - Column 4: First moment
$h_z$ - Column 5: Inertia
$I_{xx}$ - Column 6: Inertia
$I_{xy}$ - Column 7: Inertia
$I_{xz}$ - Column 8: Inertia
$I_{yy}$ - Column 9: Inertia
$I_{yz}$ - Column 10: Inertia
$I_{zz}$
Each row corresponds to one component of the 6D wrench:
and each column corresponds to one inertial parameter in:
For a revolute joint with axis
where
This gives a
giving the extended parameter vector
The complete friction model is:
And the total joint torque becomes:
where the first term represents the inertial torque contribution and the latter terms represent Coulomb and viscous friction.
The tool generates multi-frequency sinusoidal trajectories to ensure rich excitation:
- Terminal joint — Sum of 3–5 sinusoids with frequencies 0.1–0.6 Hz and amplitudes scaled to stay within joint limits
- Non-terminal joints — Low-amplitude multi-frequency sinusoids to improve regressor conditioning
- Incommensurate frequencies — Frequencies are chosen to avoid periodicity and ensure diverse kinematic states
Velocities and accelerations are computed analytically (exact derivatives of position sinusoids), avoiding numerical differentiation errors.
The parameter vector θ is recovered via least-squares:
Where np.linalg.lstsq with rcond=1e-10 for robust least-squares, which handles rank-deficient regressor matrices gracefully.
Due to the structure of the dynamics equations, not all 10 inertial parameters are independently identifiable from single-joint torque measurements. The identifiable combinations are called base parameters. The tool reports the number of base parameters (matrix rank), which should be close to 10 for well-conditioned data.
robot-sysid/
├── robot_sysid/ # Main package
│ ├── __init__.py # Package initialization
│ ├── cli.py # CLI entry point (argparse, pipeline orchestration)
│ ├── parser.py # MuJoCo XML model loading and validation
│ ├── trajectory.py # Excitation trajectory generation
│ ├── regressor.py # Spatial dynamics regressor computation (SymPy)
│ ├── simulator.py # MuJoCo simulation and data collection
│ ├── solver.py # Least-squares parameter identification
│ └── export.py # JSON, XML, and CSV exporters
├── examples/
│ └── kinova/ # Kinova arm reference example
│ ├── model/ # MuJoCo XML models and STL meshes
│ ├── validate_*.py # Validation scripts
│ └── README.md # Kinova example documentation
├── tests/ # Unit and property-based tests
│ ├── test_parser.py
│ ├── test_trajectory.py
│ ├── test_regressor.py
│ ├── test_simulator.py
│ ├── test_solver.py
│ └── test_export.py
├── pyproject.toml # Package metadata and dependencies
└── README.md # This file
Possible causes:
- Insufficient excitation — trajectory may not be rich enough
- Poor initial inertial guesses in the XML
- Incorrect mesh references or joint definitions
Solutions:
- Increase trajectory duration:
--duration 30 - Try a different random seed:
--seed 123 - Verify that the model's joint limits are reasonable
- Check that mesh files are correctly referenced
Possible causes:
- Insufficient excitation — regressor matrix is poorly conditioned
- Trajectory frequencies are too similar (near-periodic motion)
Solutions:
- Increase trajectory duration:
--duration 20 - Try a different random seed to get different frequencies:
--seed 456 - Check that non-terminal joints are moving (they should have low-amplitude excitation)
Possible causes:
- Unreasonable initial inertial guesses (e.g., negative mass, non-positive-definite inertia)
- Joint limits are too restrictive, causing large control forces
- Trajectory amplitude is too large
Solutions:
- Check that the initial model has physically plausible inertial parameters
- Verify that joint limits in the XML are not overly restrictive
- Reduce trajectory amplitude by tightening joint limits in the XML
If many trajectory points are clamped when exporting for Damiao motors:
- The trajectory may exceed motor limits
- Consider using a motor with higher limits (e.g., DM6006 instead of DM4340)
- Reduce trajectory duration or amplitude by adjusting joint limits in the XML
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
pytest tests/
# Run specific test file
pytest tests/test_solver.py
# Run with verbose output
pytest tests/ -v
# Run property-based tests with more examples
pytest tests/ --hypothesis-seed=42- Unit tests — Test specific examples, edge cases, and error conditions using pytest
- Property-based tests — Test universal correctness properties using hypothesis (100+ examples per property)
- Integration tests — End-to-end tests using the Kinova example model
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass:
pytest tests/ - Submit a pull request
See the examples/kinova/ directory for a complete reference example demonstrating how to use the tool with a Kinova 7-DOF robotic arm. The example includes:
- MuJoCo XML models with ground truth and guessed inertial parameters
- Validation scripts demonstrating the full pipeline
- Detailed README with usage examples and output interpretation
For more details, see examples/kinova/README.md.
MIT
If you use this tool in your research, please cite:
@software{robot_sysid,
title = {robot-sysid: A Generic Robot System Identification Tool},
author = {Cai, Yuze},
year = {2026},
url = {https://github.com/curryabalone/Robotic-Arm-System-ID-}
}