Auto-calibrate physics parameters for CAD robots, then write production-ready USD for NVIDIA Isaac Sim.
cad-physics-tuner closes the sim-to-real gap by optimising joint stiffness, damping, friction, mass, inertia, and contact offsets so that a simulated robot matches trajectories recorded on real hardware. It wraps Isaac Lab's URDF converter and exposes a clean Python API and CLI that work even without a full Isaac Sim installation.
- Features
- Requirements
- Installation
- Quick Start
- Pipeline Overview
- Dataset Format
- Tunable Parameters
- Optimisation Methods
- Configuration Reference
- Examples
- Development
- License
- URDF → USD conversion via Isaac Lab's
UrdfConverter(graceful no-op stub when Isaac Sim is absent) - Physics auto-calibration using
scipy.optimize— RMSE position tracking error, velocity error, and force residuals are minimised simultaneously - Flexible simulator interface — wire in Isaac Sim physics steps, MuJoCo, or any callable that maps parameter vectors to joint trajectories
- Write-back to USD — optimised values are persisted directly on the USD stage using
pxr.UsdPhysics/PhysxSchema - JSON calibration report with initial error, final error, improvement percentage, iteration count, and elapsed time
- Zero Isaac Sim requirement for unit tests and offline development — all core logic runs on NumPy + SciPy only
| Dependency | Version |
|---|---|
| Python | ≥ 3.10 |
| NumPy | ≥ 1.24 |
| SciPy | ≥ 1.10 |
| Isaac Sim / Isaac Lab | Optional — enables real conversion and USD write-back |
Linux / macOS
# From source (recommended during development)
git clone https://github.com/your-org/cad-physics-tuner.git
cd cad-physics-tuner
pip install -e .
# With dev extras (pytest, ruff, coverage)
pip install -e ".[dev]"Windows (PowerShell)
# From source
git clone https://github.com/your-org/cad-physics-tuner.git
cd cad-physics-tuner
pip install -e .
# With dev extras
pip install -e ".[dev]"Isaac Sim note: if
omni.isaac.labis on the Python path, the realUrdfConverterand USD stage writer are used automatically. Without it, the package runs in stub mode: conversion produces a placeholder.usdfile and USD writes are stored in memory only.
Convert a URDF to USD
cad-physics-tuner convert --input robot.urdf --output-dir out/
# or
python -m cad_physics_tuner convert --input robot.urdf --output-dir out/Run the full calibration pipeline
Linux / macOS
cad-physics-tuner calibrate \
--input robot.urdf \
--dataset real_traj.csv \
--params stiffness:/robot/joints/j1:1000:100:1e5 \
damping:/robot/joints/j1:50:1:500 \
--method L-BFGS-B \
--max-iterations 300 \
--output-dir out/ \
--report out/report.jsonWindows (PowerShell)
cad-physics-tuner calibrate `
--input robot.urdf `
--dataset real_traj.csv `
--params stiffness:/robot/joints/j1:1000:100:1e5 `
damping:/robot/joints/j1:50:1:500 `
--method L-BFGS-B `
--max-iterations 300 `
--output-dir out/ `
--report out/report.jsonTip: PowerShell uses backticks (
`) for line continuation instead of backslashes.
Each --params entry follows the format NAME:PRIM_PATH:INITIAL:LOWER:UPPER, for example:
stiffness:/robot/joints/shoulder_pan:1000:100:1e5
CLI flags
| Flag | Default | Description |
|---|---|---|
--input |
— | Path to the input URDF file |
--dataset |
— | CSV file of real robot trajectories |
--params |
— | One or more NAME:PRIM:INIT:LO:HI specs |
--method |
L-BFGS-B |
Optimisation algorithm |
--max-iterations |
200 |
Optimiser iteration budget |
--dt |
0.01 |
Dataset sampling period in seconds |
--output-dir |
. |
Directory for all outputs |
--output |
<output-dir>/robot_physics.usd |
Path for the tuned USD |
--report |
(omit to skip) | Path for the JSON calibration report |
--fix-base |
false |
Fix the robot base in place (convert sub-command) |
--no-instanceable |
false |
Disable instanceable USD output (convert) |
--log-level |
INFO |
Logging verbosity: DEBUG, INFO, WARNING, ERROR |
One-shot pipeline with Orchestrator
from cad_physics_tuner import Orchestrator
from cad_physics_tuner.calibrator import CalibrationDataset, PhysicsParam
# 1. Describe the parameters to tune
params = [
PhysicsParam("stiffness", "/robot/joints/j1", initial=1000, bounds=(100, 1e5)),
PhysicsParam("damping", "/robot/joints/j1", initial=50, bounds=(1, 500)),
]
# 2. Load real robot data
dataset = CalibrationDataset.from_csv("real_traj.csv", dt=0.01)
# 3. Provide a simulator callable
def simulator(x, ds):
"""Run your physics engine with the parameter vector x.
x[0] = stiffness, x[1] = damping (in declaration order)
Returns (q_sim, qd_sim, f_sim) — any may be None.
"""
q_sim = my_physics_step(x, ds)
return q_sim, None, None
# 4. Run the pipeline
orch = Orchestrator(input_path="robot.urdf", output_dir="out/")
usd_path, report_path = orch.run(
dataset=dataset,
params=params,
simulator_fn=simulator,
method="L-BFGS-B",
output_path="out/robot_physics.usd",
report_path="out/report.json",
max_iterations=200,
)
print(f"Tuned USD → {usd_path}")
print(f"Report → {report_path}")Step-by-step with individual components
from cad_physics_tuner.converter import UrdfToUsdConverter
from cad_physics_tuner.calibrator import PhysicsCalibrator, CalibrationDataset, PhysicsParam
from cad_physics_tuner.physics_writer import PhysicsWriter
# Convert
converter = UrdfToUsdConverter(output_dir="out/", fix_base=True)
result = converter.convert("robot.urdf") # result.usd_path
# Calibrate
params = [PhysicsParam("stiffness", "/robot/joints/j1", 1000, (100, 1e5))]
dataset = CalibrationDataset.from_csv("real_traj.csv")
cal = PhysicsCalibrator(params=params, simulator=simulator)
report = cal.calibrate(dataset) # CalibrationResult
# Write back
writer = PhysicsWriter(result.usd_path)
writer.apply_optimised_params(report.optimised_params, output_path="out/tuned.usd")URDF / CAD file
│
▼
┌─────────────────────┐
│ UrdfToUsdConverter │ ← wraps Isaac Lab UrdfConverter
│ (convert step) │
└─────────┬───────────┘
│ .usd
▼
┌─────────────────────┐ real robot
│ PhysicsCalibrator │ ◄── trajectory CSV
│ (calibrate step) │
│ │
│ scipy.optimize │
│ ┌───────────────┐ │
│ │ simulator_fn │ │ ← your physics engine callback
│ └───────────────┘ │
└─────────┬───────────┘
│ optimised params
▼
┌─────────────────────┐
│ PhysicsWriter │ ← pxr.UsdPhysics / PhysxSchema
│ (save step) │
└─────────┬───────────┘
│
tuned .usd + report.json
Pass a CSV file with no header row. Columns are interpreted based on the total column count, which must be a multiple of the number of joints N:
| Columns | Interpretation |
|---|---|
N |
Joint positions only |
2 × N |
Joint positions, then joint velocities |
3 × N |
Joint positions, velocities, then joint efforts |
Example for a 7-DOF arm with positions + velocities (14 columns, 100 timesteps):
0.00, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.00, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06
0.01, 0.11, 0.21, 0.31, 0.41, 0.51, 0.61, 0.01, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06
...
You can also construct a dataset directly in Python:
import numpy as np
from cad_physics_tuner.calibrator import CalibrationDataset
dataset = CalibrationDataset(
q_real=np.load("q.npy"), # shape (T, N)
qd_real=np.load("qd.npy"), # optional, shape (T, N)
f_real=np.load("f.npy"), # optional, shape (T, N)
dt=0.01,
)The following parameter names are understood by PhysicsParam and PhysicsWriter. Each maps to a USD attribute on the specified prim path.
| Name | USD Attribute | Typical Prim |
|---|---|---|
stiffness |
drive:angular:physics:stiffness |
Joint prim |
damping |
drive:angular:physics:damping |
Joint prim |
max_force |
drive:angular:physics:maxForce |
Joint prim |
static_friction |
physics:staticFriction |
Physics Material prim |
dynamic_friction |
physics:dynamicFriction |
Physics Material prim |
restitution |
physics:restitutionCoefficient |
Physics Material prim |
mass |
physics:mass |
Rigid body prim |
inertia_x |
physics:diagonalInertia_x |
Rigid body prim |
inertia_y |
physics:diagonalInertia_y |
Rigid body prim |
inertia_z |
physics:diagonalInertia_z |
Rigid body prim |
contact_offset |
physxCollision:contactOffset |
Collision prim |
rest_offset |
physxCollision:restOffset |
Collision prim |
You can also pass any raw USD attribute name directly as the name field if it is not in the schema above.
| Method | Key | Notes |
|---|---|---|
| L-BFGS-B | L-BFGS-B |
Gradient-based, best for smooth objectives with bounded params |
| Nelder-Mead | Nelder-Mead |
Gradient-free simplex method, robust to noisy objectives |
| Differential Evolution | differential_evolution |
Global search, good for multi-modal landscapes, slower |
| Grid search | grid |
Exhaustive grid over [lower, upper] for each parameter |
PhysicsParam(
name="stiffness", # key from PARAM_SCHEMA or raw USD attribute name
prim_path="/robot/joints/j1",# USD prim path
initial=1000.0, # starting value
bounds=(100.0, 1e5), # (lower, upper) search bounds
)CalibrationDataset(
q_real=..., # np.ndarray (T, N) — required
qd_real=..., # np.ndarray (T, N) — optional
f_real=..., # np.ndarray (T, N) — optional
dt=0.01, # sampling period in seconds
)Orchestrator(
input_path="robot.urdf",
output_dir="out/", # defaults to input file's directory
fix_base=False, # passed to UrdfToUsdConverter
make_instanceable=True, # passed to UrdfToUsdConverter
){
"optimised_params": {
"/robot/joints/j1": { "stiffness": 1987.4, "damping": 78.2 }
},
"initial_error": 0.043210,
"final_error": 0.001234,
"improvement_pct": 97.1,
"n_iterations": 42,
"elapsed_seconds": 3.7,
"success": true,
"message": "Optimization terminated successfully",
"error_history": [0.043210, 0.031, ...]
}The examples/calibrate_franka.py script demonstrates the full pipeline without Isaac Sim. It generates synthetic "real" trajectories from known ground-truth stiffness and damping values, then verifies the calibrator recovers them:
python examples/calibrate_franka.pyExpected output:
============================================================
cad-physics-tuner – Franka Calibration Example
============================================================
Ground-truth stiffness : 2000.0
Ground-truth damping : 80.0
Optimised stiffness : 1999.87
Optimised damping : 80.03
Initial error : 0.048312
Final error : 0.000041
Improvement : 99.9 %
Tuned USD : examples/_output/franka_physics.usd
Report : examples/_output/calibration_report.json
============================================================
def my_isaac_sim_fn(x: np.ndarray, dataset):
"""
x — 1-D array of parameter values in the order params were declared.
dataset — CalibrationDataset with real robot data.
Returns (q_sim, qd_sim, f_sim); unused channels may be None.
"""
set_joint_stiffness(x[0])
set_joint_damping(x[1])
q_sim, qd_sim = run_physics_steps(dataset)
return q_sim, qd_sim, None# Install dev dependencies (Linux / macOS / PowerShell)
pip install -e ".[dev]"
# Run tests
pytest
# Run tests with coverage
pytest --cov=cad_physics_tuner --cov-report=term-missing
# Lint
ruff check .All tests pass without Isaac Sim or pxr installed.
MIT — see LICENSE.