Tutorial 14: System Identification of a Franka#

Tutorial

Note

The System Identification extension is in beta. Its workflow, run contracts, exported run specifications, and APIs are not guaranteed to stay future compatible.

This tutorial runs a reproducible, free-space system-identification experiment for a fixed-base Franka Panda. To make the optimization behavior visible, it begins with a deliberately over-frictional working asset. The source asset is preserved; only a separate tutorial overlay is degraded. After recovering joint friction, the tutorial diagnoses the remaining held-out residual and runs a separately bounded joint-damping phase. Each phase starts from the previously persisted candidate and must pass its own replay gate.

The example uses recorded MCAP telemetry, explicit command alignment, separate training and validation motions, Newton MuJoCo rollouts, and CMA-ES. Optimization, USD mutation, replay, and promotion remain separate operations so that every stage is reviewable and reversible.

Franka system identification in Isaac Sim, progressing from an excited reference through a mismatched initial model to an identified model that reproduces the reference pose.

The walkthrough follows the same progression: reference excitation and telemetry on the left, the deliberately poor initial model in orange, and the recovered candidate in green.#

After completing this tutorial, you will be able to:

  • Construct a controlled poor initialization without modifying a source asset.

  • Validate telemetry mapping, timing, excitation, and effort semantics.

  • Configure and export a bounded friction-only RunSpec.

  • Solve, persist, and independently replay one parameter phase.

  • Diagnose held-out residuals and configure a conservative second phase.

  • Compare candidates with a predeclared acceptance policy.

  • Decide when to promote a candidate and when to collect new excitation data.

  • Interpret predictive improvement without overstating physical identifiability.

For the complete feature reference, see System Identification Extension.

60-80 minute tutorial, excluding optimization time

Reference outcome#

The tutorial intentionally authors an excessive 1.0 N*m*s/rad of Newton joint friction on all seven arm joints in a working overlay before fitting. The independently reloaded friction candidate reduces held-out normalized cost by 42.7%. Position, velocity, and torque RMSE improve by 8.3%, 13.7%, and 24.4%, respectively.

A second phase opens only seven joint-damping scales. Its independently reloaded candidate reduces torque RMSE from 1.6573 N*m to 1.3946 N*m and normalized cost by another 28.9%. Position and velocity RMSE regress slightly relative to the friction candidate, by 2.0% and 1.1%, while remaining better than the poor seed. The final decision therefore demonstrates an explicit application tradeoff instead of selecting the lowest scalar objective without review.

This is a stress-test initialization, not a recommended engineering prior. It demonstrates a staged tuning cycle from a meaningfully poor model while keeping every experiment stable and reviewable.

Prerequisites#

  • A built NVIDIA Isaac Sim source checkout.

  • isaacsim.robot_setup.sysid.ui enabled. Open Window > Extensions, search for system identification, and enable isaacsim.robot_setup.sysid.ui. The interactive extension automatically enables the isaacsim.robot_setup.sysid backend.

  • The packaged Franka tutorial from the Content Browser at IsaacSim/Samples/SystemIdentification/franka_sysid_tutorial/. Use these exact paths:

    • Stage: assets/franka_poor_seed.usda, which references the packaged source asset assets/franka.usda. Copy both.

    • RunSpec: config/franka_friction_run_spec.json.

    • MCAP recording: data/bag_0.mcap.

    • Topic mapping: data/franka_sysid_topic_map.yaml.

    • Collection manifest: data/collection_manifest.json.

    • Reference result: results/franka_friction_result.json.

    The fixed-base stage articulation resolves to /World/envs/env_0/panda_instanceable.

  • A CUDA-capable GPU for the Newton MuJoCo population rollout.

  • A local working directory for RunSpecs, reports, checkpoints, and output USDs. Copy the complete franka_sysid_tutorial directory and preserve its packaged subdirectory layout, because the packaged RunSpec resolves ../assets, ../data, and ../results relative to its own location under config/. Then add one empty runs subdirectory for everything this tutorial generates. Do not modify the Samples originals.

    This tutorial refers to that working directory as C:\sysid_tutorial on Windows and ~/sysid_tutorial on Linux:

    sysid_tutorial/
    |-- assets/    franka_poor_seed.usda, franka.usda   (packaged inputs)
    |-- config/    franka_friction_run_spec.json        (packaged reference)
    |-- data/      bag_0.mcap, topic map, manifest      (packaged inputs)
    |-- results/   franka_friction_result.json          (packaged reference)
    `-- runs/      everything you export, solve, apply, and replay
    

The reference recording contains 9,439 samples for seven arm joints at approximately 30 Hz. It provides joint position, velocity, command, and link-side torque.

Important

System Identification consumes excitation data; it does not generate or certify safe robot commands. Validate joint, velocity, acceleration, effort, collision, and workspace limits before executing a trajectory on hardware. See Trajectory generation and excitation for external experiment-design resources.

Step 1: Open the deliberately poor working asset#

Copy the complete IsaacSim/Samples/SystemIdentification/franka_sysid_tutorial/ directory into your working directory as described in Prerequisites, then open assets/franka_poor_seed.usda and confirm that the articulation initializes. The packaged stage already carries the over-frictional values below, so there is nothing to author here; the table records what was degraded and why. Do not edit the Samples originals.

franka_poor_seed.usda is a thin overlay over the packaged assets/franka.usda, which it references rather than edits. The source asset authors no Newton joint friction at all, so every arm joint would otherwise run at the solver default. The overlay selects the Newton physics variant and raises all seven arm joints to 1.0 N*m*s/rad, leaving stiffness, damping, armature, masses, inertias, and controller settings untouched. That isolates the injected error to the single family the tutorial optimizes.

Property

Source asset franka.usda

Poor working overlay

Joint 1-7 Newton friction

Not authored

1.0 N*m*s/rad

Other physical properties

Unchanged

Unchanged

Friction is authored as newton:friction under NewtonJointAPI. That is the attribute both the Newton solver and System Identification read; a plain jointFriction value is inert to both.

The poor seed remains numerically stable, but its excess friction creates a measurable trajectory and torque mismatch. This controlled construction is preferable to changing several coupled families at once because the cause of the initial error is known.

Warning

The 1.0 N*m*s/rad seed is a deliberately excessive tutorial value, not a measurement of a physical Franka, and the recording carries no authored friction value for the optimizer to rediscover. The recovered result is evaluated as a better predictive model for this recording, not as a certificate of true joint friction.

Step 2: Select the robot and configure the rollout#

With isaacsim.robot_setup.sysid.ui enabled, open Tools > Robotics > Asset Editors > System Identification. The panel is a six-stage workflow: 1 Robot, 2 Data, 3 Parameters, 4 Check, 5 Solve, and 6 Results. Work through the stages in that order. Select a stage from the tab strip at the top of the panel, or use Back and Next at the bottom.

On 1 Robot, at the top of the tab:

  1. Set Physics engine to Newton (MuJoCo).

  2. Set Robot prim path to /World/envs/env_0/panda_instanceable. The field defaults to /World/envs/env_0, which is the environment prim rather than the articulation, so type the full path.

System Identification Robot tab with Physics engine set to Newton (MuJoCo) and the Franka articulation in the Robot prim path field.

The list pairs an engine with a solver rather than offering the two separately. Both Newton entries are the same engine with a different solver, so picking Newton (MuJoCo) chooses Newton and its MuJoCo solver in one step.#

Important

Physics engine does not change the physics runtime of the running application. Selecting Newton (MuJoCo) does not migrate your stage to Newton, does not rewrite the stage’s PhysicsScene, and does not alter how the robot behaves when you press Play. The viewport and the timeline keep using whatever physics the stage is already configured for.

What the setting does control is which engine the System Identification job simulates its candidate parameters on, both for the panel’s own Run on 5 Solve and through the simulation.engine key of the RunSpec you export in Step 7, which the headless tools in Steps 8 through 15 consume.

It also feeds several things inside the panel, so treat it as a decision to make before you configure the rest of the run rather than one to revisit later. It is part of the check signature, so changing it invalidates a completed 4 Check the same way the Step 7 solver settings do. It decides whether the Newton block below is shown at all, drives the backend-compatibility hint on 5 Solve, and determines whether the timeline has to be playing: only Isaac Sim (in-app) requires it, so the Newton rollouts in this tutorial run without pressing Play.

Expand Advanced simulation settings and set the Newton rollout controls:

Setting

Reference value

Newton device

cuda

Controller

pd

Controller feedforward

inverse_dynamics

Effort clamp

max_effort

CUDA graph capture

Selected (default)

Advanced simulation settings expanded, showing the Newton device, controller, feedforward, effort clamp, and CUDA graph capture controls.

The Newton block appears only while Physics engine is a Newton selection.#

Leave In-app physics backend and Missing explicit actuator policy at their defaults; they apply to the Isaac Sim (in-app) engine. Leave Actuator runtime at auto, which resolves to mixed actuator ownership for standalone MuJoCo. The Offline stepping control and the clone settings are hidden for this run because they belong to the Isaac Sim (in-app) engine; the equivalent RunSpec key is simulation.offline_stepping.

Newton MuJoCo supports batched, derivative-free population evaluation. The differentiable Newton Featherstone rollout is a separate headless-only path with a narrower supported parameter set; it is not used by this CMA-ES walkthrough.

Leave CUDA graph capture enabled for repeated, fixed-shape CMA-ES evaluations. The first evaluation warms the kernels and solver storage, the next captures that shape, and later evaluations reuse the graph. Each captured and uncaptured MuJoCo rollout resets rollout-local warm-start, actuator-activation, applied-force, and control buffers before restoring the initial joint state, so a preceding candidate batch does not seed the next one.

In a reference Franka run with 16 candidates and 800 steps, graph replay reduced median evaluation time from 13.59 seconds uncaptured to 0.515 seconds captured, a 26.4x speedup after warmup. The ratio is specific to that workload and hardware. For a new stage or RunSpec, compare a captured result with an uncaptured result using declared numerical tolerances. Small GPU floating-point differences are expected; disable capture if the difference exceeds the acceptance threshold rather than requiring bitwise equality.

Step 3: Load and verify telemetry#

Select 2 Data. The source fields sit at the top of the tab, above Load Trajectory.

The Command alignment (s) field belongs to this group because changing it forces a reload. The telemetry diagnostic estimates a common timing offset between commands and measured response, and the reference recording produced:

Diagnostic

Value

Common lag (samples)

16.0825

Common lag (seconds)

0.536075

Joint-to-joint residual spread

0.5336 samples

Those numbers come from an uncorrected load. To measure them yourself rather than taking them on faith, leave Command alignment (s) at its default of 0 for a first Load Trajectory, read the diagnostic, and then enter the offset and reload. The steps below go straight to the corrected load.

  1. Set Source type to MCAP.

  2. Set Data source path to your local copy of data/bag_0.mcap.

  3. Set Mapping config to data/franka_sysid_topic_map.yaml.

  4. Set Chunk manifest to data/collection_manifest.json.

  5. Set Command alignment (s) to 0.5360748729665944.

  6. Click Load Trajectory.

Enter all sixteen digits. The field displays four decimals, so it will read 0.5361 after you commit it, but it stores and exports the full value.

Command alignment corrects the recorded series before fitting. Actuator command delay changes the simulated model, so leave the actuator command-delay parameter disabled for this phase. Applying both to the same logging offset would double-count the effect.

System Identification Data tab after loading the Franka MCAP recording, showing the source fields, analysis window, and telemetry summary.

Analysis window autofills to the full recording span, and the Telemetry summary reports the sample count, channels, and torque semantics.#

Confirm that the Telemetry summary reads 9439 samples, 7 joints, 30.00 Hz, lists channels=position, velocity, command, torque, and ends with torque semantics=link_side. The torque-semantics field matters because motor-side, link-side, commanded, and measured efforts are not interchangeable. Also verify joint order, signs, units, timestamp gaps, clipping, and long flat intervals.

The summary also reports 1 warning(s) at this point, and the stage header reads 1 train / 0 validation chunk(s). Both describe the same condition: no validation holdout is configured yet. Step 4 resolves it.

Step 4: Separate training and validation#

Stay on 2 Data and scroll to Dataset partitioning.

Under Automatic validation holdout, leave Enabled cleared. It is cleared by default, and manual chunks take precedence over the automatic holdout in any case.

The packaged recording ships a collection manifest but no phase_events.jsonl companion, so no phase chunks are imported and Configured manual chunks starts empty. Enter the two tutorial motions with the Manual chunk editor. Add Chunk captures whatever is currently in Analysis window, so set the window first for each chunk:

Name

Role

Excitation

Start time (s)

End time (s)

coupled_multisine_fast

Train

sine

1781120740.1658437

1781120763.886561

coupled_multisine_validation

Validation

sine

1781120792.565396

1781120819.9389462

Dataset partitioning section with the automatic holdout cleared and the manual chunk editor filled in for the training multisine.

Analysis window holds the training chunk bounds while the Manual chunk editor carries its name, role, and excitation tag.#

For each row, set Start time (s) and End time (s) in Analysis window, then set Name, Role, and Excitation in Manual chunk editor, then click Add Chunk. After adding both chunks, set Analysis window to span them both: 1781120740.1658437 to 1781120819.9389462. The exported RunSpec stores that span as telemetry.time_window, and the reference results later in this tutorial were produced with it.

Configured manual chunks table listing one train chunk and one validation chunk with their sample counts and durations.

The chunk table snaps each requested window to sample boundaries: 711 samples over 23.667 s for the training chunk and 821 samples over 27.333 s for the validation chunk.#

The training multisine contributes to the optimizer. The validation multisine is withheld from optimization and is used for model selection and the final replay. Both motions exercise all seven joints, but they do not guarantee that every selected physical parameter is independently identifiable.

Step 5: Select the friction parameter phase#

Select 3 Parameters.

  1. Expand Additional parameter families and select Edit bounds (Initial/Min/Max). The parameter table replaces its single Value column with Initial, Min, and Max columns.

  2. Under Parameters to identify, click Friction in the Families row. This selects the friction row of every joint and targets that family for bulk bounds.

  3. In Family bounds, enter 1.0 for Initial, 0.1 for Min, and 1.5 for Max, then click Apply.

Family

Count

Initial

Minimum

Maximum

Joint friction

7

1.0

0.1

1.5

Parameters tab with Edit bounds (Initial/Min/Max) enabled, the friction family selected, and the applied bounds visible on the Joint 1 friction row.

Each joint group separates Solver calibration rows (stiffness, damping, integral) from Physical (SysID) rows (friction, armature).#

Confirm that the stage header reads 7 selected and that the status line under Family bounds reads Applied bounds to 7 selected friction rows. Expand a joint group to verify that its Friction row carries 1.0, 0.1, and 1.5.

The scale value 1.0 represents the deliberately poor 1.0 N*m*s/rad property carried by the poor-seed overlay from Step 1. The bounds therefore correspond to 0.1-1.5 N*m*s/rad on this working asset.

Do not add stiffness, damping, or armature to this first phase simply because the UI permits it. Adding coupled families would make a larger numerical improvement harder to interpret and would no longer match the controlled error introduced in Step 1. The later damping phase starts only after the friction candidate has been persisted, replayed, and accepted as its own baseline.

Step 6: Run the identifiability check#

Select 4 Check and click Run Check.

Check tab report showing the telemetry summary, a rank 7 of 7 presolve line, and identifiable verdicts for the friction parameters.

For the friction phase, a green banner and an ok stage badge mean the run gate is satisfied. Scroll the verdict list to see all seven joints.#

At this point the friction-only regressor reports rank 7/7, condition 1.04e+00 (target: measured_torque), and all seven rows return identifiable. A count such as 7 selected is not an identifiability result; use the per-parameter verdicts, rank, conditioning, and sensitivity instead.

Note

Re-running the check after Step 7 reports condition 1.02e+00 instead. The cause is Max rollout steps, which Step 7 raises from its default of 300 to 800: the presolve regresses over that many samples per chunk, so the check here fits 300 of the 711 training samples while the Step 7 re-run fits all of them. Both results are correct for their own sample count, and neither changes the verdicts.

Step 7: Configure the optimizer and export#

Select 5 Solve. Set the three controls at the top of the tab, then expand Advanced solver settings for the rest:

Section

Setting

Reference value

Top of tab

Optimizer

CMA-ES

Top of tab

Max iterations

48

Top of tab

Max rollout steps

800

Advanced solver settings

CMA-ES sigma

0.2

Advanced solver settings

CMA-ES batch size

16

Advanced solver settings

CMA-ES seed

20260730

Advanced solver settings

Weight: position

1.0

Advanced solver settings

Weight: velocity

1.0

Advanced solver settings

Weight: torque

0.01

Solve tab with CMA-ES selected and the advanced solver settings expanded to show sigma, batch size, seed, and the three residual weights.

CMA-ES batch size caps how many candidates are evaluated per rollout wave. There is no population field: CMA-ES derives its population from the number of optimized parameters.#

Now return to 4 Check and click Run Check once more. Three of the settings above belong to the check signature and invalidated the Step 6 result when you changed them: Optimizer, Max rollout steps, and the three residual weights. Max iterations, CMA-ES sigma, CMA-ES batch size, and CMA-ES seed are not part of the signature and leave an existing check valid. For the friction phase the 5 Solve stage badge changes from not ready to ready once the check passes again.

Back on 5 Solve, scroll to Outputs & stage changes and click Export RunSpec. Save it as franka_friction_run_spec.json in the runs subdirectory of your working directory. The packaged config/franka_friction_run_spec.json is a reference copy for comparison; every command in this tutorial reads the one you just exported from runs.

Note

One key the reference uses has no control in the panel: solver.cma_population_size. The packaged RunSpec sets it to 16, while a GUI export leaves it unset. CMA-ES then derives its population from the seven optimized parameters, which yields 9, and CMA-ES batch size clamps to that same 9. To reproduce the iteration count and recovered scales reported below, open the exported file and add "cma_population_size": 16 to its solver section. This is the same RunSpec-only situation as simulation.offline_stepping in Step 2.

Outputs and stage changes section with the writeback checkboxes and the Export RunSpec button.

Review the writeback flags before exporting. Apply optimized parameters to USD and Write USD provenance are selected by default.#

Review every input path, output path, bound, and writeback flag. Point input paths at your local copies of franka_sysid_tutorial/assets/franka_poor_seed.usda, franka_sysid_tutorial/data/bag_0.mcap, franka_sysid_tutorial/data/franka_sysid_topic_map.yaml, and franka_sysid_tutorial/data/collection_manifest.json. Keep output paths local. Store the exported file with its topic mapping, collection manifest, source revision, and input hashes.

Step 8: Establish the poor-seed baseline#

Run the analyzer before optimization. Every command from here on reads its inputs from the packaged subdirectories of your working directory copy and writes everything new into runs, so no command overwrites a Samples original. Run them from the root of your NVIDIA Isaac Sim source checkout.

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_analyze.py \
    --run-spec ~/sysid_tutorial/runs/franka_friction_run_spec.json \
    --stage ~/sysid_tutorial/assets/franka_poor_seed.usda \
    --result-json ~/sysid_tutorial/runs/poor_seed_baseline.json \
    --mode baseline
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_analyze.py `
    --run-spec C:\sysid_tutorial\runs\franka_friction_run_spec.json `
    --stage C:\sysid_tutorial\assets\franka_poor_seed.usda `
    --result-json C:\sysid_tutorial\runs\poor_seed_baseline.json `
    --mode baseline

The aligned held-out baseline was:

Metric

Value

Normalized cost

0.008066

Position RMSE

0.004443 rad

Velocity RMSE

0.017925 rad/s

Torque RMSE

2.1922 N*m

Keep this report unchanged. A post-solve value without the same-stage, same-RunSpec baseline cannot establish improvement.

Step 9: Solve, then apply to a new USD#

Run the exported specification:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_solve.py \
    --run-spec ~/sysid_tutorial/runs/franka_friction_run_spec.json \
    --result-json ~/sysid_tutorial/runs/franka_friction_result.json \
    --checkpoint-json ~/sysid_tutorial/runs/franka_friction_checkpoint.json
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_solve.py `
    --run-spec C:\sysid_tutorial\runs\franka_friction_run_spec.json `
    --result-json C:\sysid_tutorial\runs\franka_friction_result.json `
    --checkpoint-json C:\sysid_tutorial\runs\franka_friction_checkpoint.json

Note

The exported RunSpec leaves Apply optimized parameters to USD and Write USD provenance enabled, so this solve does author the selected values onto the stage it opens. Nothing reaches disk, because writing the stage is gated separately by --save-stage, which the command above omits. That is why the asset mutation in the next command is still an independent, reviewable step. The packaged config/franka_friction_run_spec.json ships with both flags cleared; either setting reproduces the result below, since the flags do not affect the optimization itself.

The selected candidate was the last accepted candidate at iteration 47. Review selected_status rather than assuming the final attempted generation is accepted. The selected scales were:

Joint

Initial scale

Recovered scale

Bound status

Joint 1

1.000

0.214

Interior

Joint 2

1.000

0.105

Interior

Joint 3

1.000

0.848

Interior

Joint 4

1.000

0.184

Interior

Joint 5

1.000

0.172

Interior

Joint 6

1.000

0.147

Interior

Joint 7

1.000

0.156

Interior

No selected value is classified as a bound hit. Joint 2 is close to the lower bound, so extending the search range would be a useful sensitivity study before interpreting its value physically.

Author the selected vector onto a new stage:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_apply_theta_to_stage.py \
    --run-spec ~/sysid_tutorial/runs/franka_friction_run_spec.json \
    --result-json ~/sysid_tutorial/runs/franka_friction_result.json \
    --stage ~/sysid_tutorial/assets/franka_poor_seed.usda \
    --output-stage ~/sysid_tutorial/runs/franka_friction_candidate.usda \
    --summary-json ~/sysid_tutorial/runs/franka_friction_apply_summary.json
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_apply_theta_to_stage.py `
    --run-spec C:\sysid_tutorial\runs\franka_friction_run_spec.json `
    --result-json C:\sysid_tutorial\runs\franka_friction_result.json `
    --stage C:\sysid_tutorial\assets\franka_poor_seed.usda `
    --output-stage C:\sysid_tutorial\runs\franka_friction_candidate.usda `
    --summary-json C:\sysid_tutorial\runs\franka_friction_apply_summary.json

This keeps optimization, asset mutation, and acceptance separately reviewable.

Step 10: Reload and compare held-out metrics#

Reload the persisted candidate in a fresh process and run a no-op baseline analysis:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_analyze.py \
    --run-spec ~/sysid_tutorial/runs/franka_friction_run_spec.json \
    --stage ~/sysid_tutorial/runs/franka_friction_candidate.usda \
    --result-json ~/sysid_tutorial/runs/franka_candidate_replay.json \
    --mode baseline
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_analyze.py `
    --run-spec C:\sysid_tutorial\runs\franka_friction_run_spec.json `
    --stage C:\sysid_tutorial\runs\franka_friction_candidate.usda `
    --result-json C:\sysid_tutorial\runs\franka_candidate_replay.json `
    --mode baseline

The independently reloaded candidate produced:

Held-out metric

Poor seed

Persisted candidate

Change

Normalized cost

0.008066

0.004620

42.7% lower

Position RMSE

0.004443 rad

0.004073 rad

8.3% lower

Velocity RMSE

0.017925 rad/s

0.015476 rad/s

13.7% lower

Torque RMSE

2.1922 N*m

1.6573 N*m

24.4% lower

Franka held-out error and joint-friction scales for the poor seed and independently reloaded candidate.

The left panel normalizes each error channel by its own poor-seed value. The right panel shows the large scale correction produced by the deliberately over-frictional initialization.#

Inspect time-series agreement as well as aggregate values:

Measured and simulated Franka joint-position traces from the persisted candidate on the held-out multisine.

The persisted candidate follows the held-out position motion across all seven joints. Look for phase error, bias, and localized divergence that an aggregate RMSE can hide.#

Measured and simulated Franka joint-effort traces from the persisted candidate on the held-out multisine.

Torque RMSE improves substantially but remains higher in absolute units than the state-channel RMSE. Residual effort error can include model-form, controller, effort-calibration, unmodeled friction, and timing mismatch; friction-only fitting is not expected to remove all of it.#

Step 11: Diagnose the residual and declare the next gate#

The friction candidate passes the first-phase fit gate because normalized cost and all three aggregate held-out channels improve after the result is persisted and reloaded. The effort overlay still contains structured amplitude and bias error, however. A tuning cycle should use that residual to form a new, testable phase rather than adding every available parameter at once.

For this tutorial, define an illustrative torque-refinement gate before running the second solve:

Requirement

Gate

Persisted-stage normalized cost

Lower than the friction candidate

Persisted-stage torque RMSE

At least 10% lower than the friction candidate

Position and velocity RMSE

No more than 2.5% higher than the friction candidate

Parameter bounds

No selected bound hits

Evidence source

Fresh-process replay of the written USD

These thresholds demonstrate the mechanism; define application-specific tolerances before fitting your own robot. A strict Pareto policy would instead reject any candidate that worsens any channel.

Duplicate the friction RunSpec and change its source stage to runs/franka_friction_candidate.usda. To build the second contract in the panel, open runs/franka_friction_candidate.usda and repeat Steps 2 through 7 with two changes.

On 3 Parameters, click Clear all, then click Damping in the Families row and apply these bounds in Family bounds. Damping is a Solver calibration row in each joint group, not a Physical (SysID) row:

Family

Count

Initial

Minimum

Maximum

Joint damping

7

1.0

0.5

1.5

On 5 Solve, change CMA-ES sigma to 0.15 and CMA-ES seed to 20260731 under Advanced solver settings. Keep the telemetry, chunks, command alignment, backend, controller, channel weights, Max iterations of 48, and CMA-ES batch size of 16 unchanged. Export the new contract to runs/franka_damping_run_spec.json, and add "cma_population_size": 16 to its solver section as in Step 7.

Important

4 Check does not reach an ok verdict for a damping-only phase, and it is not supposed to. With the seven damping scales selected, the presolve collapses to rank 0/0, condition inf (target: unavailable), every verdict returns unknown, the report is not ok, and the Run button stays disabled with Check reported blocking issues - resolve them and re-run Check before solving.

The reason is physical rather than a configuration error: drive damping does not appear in the link-side torque equation at the true trajectory, so the analytical presolve has no column to rank it with, and a family it cannot score keeps the unknown verdict that in turn withholds ok. The optimizer identifies damping through simulated position response instead, which the presolve cannot see.

Export RunSpec is unaffected, so export the contract from this closed-gate state and run the phase headlessly in Step 12. Do not expect the ok badge from Step 6 or the ready badge from Step 7 here; both describe the friction phase only.

Run a local sensitivity analysis before optimizing:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_analyze.py \
    --run-spec ~/sysid_tutorial/runs/franka_damping_run_spec.json \
    --stage ~/sysid_tutorial/runs/franka_friction_candidate.usda \
    --result-json ~/sysid_tutorial/runs/damping_baseline_sensitivity.json \
    --mode sensitivity
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_analyze.py `
    --run-spec C:\sysid_tutorial\runs\franka_damping_run_spec.json `
    --stage C:\sysid_tutorial\runs\franka_friction_candidate.usda `
    --result-json C:\sysid_tutorial\runs\damping_baseline_sensitivity.json `
    --mode sensitivity

All seven reference perturbations produced non-low sensitivity and matching train/validation gradient signs. This is the evidence that justifies the trial despite the closed check gate: the analytical presolve cannot score damping, but a numerical perturbation still moves the simulated response measurably. Sensitivity supports a conservative trial, but it is not proof of unique physical identification.

Step 12: Solve, persist, and replay the damping phase#

Run the second RunSpec, again without --save-stage:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_solve.py \
    --run-spec ~/sysid_tutorial/runs/franka_damping_run_spec.json \
    --result-json ~/sysid_tutorial/runs/franka_damping_result.json \
    --checkpoint-json ~/sysid_tutorial/runs/franka_damping_checkpoint.json
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_solve.py `
    --run-spec C:\sysid_tutorial\runs\franka_damping_run_spec.json `
    --result-json C:\sysid_tutorial\runs\franka_damping_result.json `
    --checkpoint-json C:\sysid_tutorial\runs\franka_damping_checkpoint.json

The reference solve selected the last accepted candidate at iteration 33:

Joint

Initial scale

Selected scale

Bound status

Joint 1

1.000

1.290

Interior

Joint 2

1.000

0.670

Interior

Joint 3

1.000

0.923

Interior

Joint 4

1.000

0.871

Interior

Joint 5

1.000

0.567

Interior

Joint 6

1.000

0.836

Interior

Joint 7

1.000

0.579

Interior

Write the selected vector to another new stage:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_apply_theta_to_stage.py \
    --run-spec ~/sysid_tutorial/runs/franka_damping_run_spec.json \
    --result-json ~/sysid_tutorial/runs/franka_damping_result.json \
    --stage ~/sysid_tutorial/runs/franka_friction_candidate.usda \
    --output-stage ~/sysid_tutorial/runs/franka_friction_damping_candidate.usda \
    --summary-json ~/sysid_tutorial/runs/franka_damping_apply_summary.json
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_apply_theta_to_stage.py `
    --run-spec C:\sysid_tutorial\runs\franka_damping_run_spec.json `
    --result-json C:\sysid_tutorial\runs\franka_damping_result.json `
    --stage C:\sysid_tutorial\runs\franka_friction_candidate.usda `
    --output-stage C:\sysid_tutorial\runs\franka_friction_damping_candidate.usda `
    --summary-json C:\sysid_tutorial\runs\franka_damping_apply_summary.json

Then reload the written stage in a new process:

./_build/linux-x86_64/release/python.sh \
    source/extensions/isaacsim.robot_setup.sysid/tools/headless_sysid_analyze.py \
    --run-spec ~/sysid_tutorial/runs/franka_damping_run_spec.json \
    --stage ~/sysid_tutorial/runs/franka_friction_damping_candidate.usda \
    --result-json ~/sysid_tutorial/runs/franka_damping_candidate_replay.json \
    --mode baseline
.\_build\windows-x86_64\release\python.bat `
    source\extensions\isaacsim.robot_setup.sysid\tools\headless_sysid_analyze.py `
    --run-spec C:\sysid_tutorial\runs\franka_damping_run_spec.json `
    --stage C:\sysid_tutorial\runs\franka_friction_damping_candidate.usda `
    --result-json C:\sysid_tutorial\runs\franka_damping_candidate_replay.json `
    --mode baseline

The optimizer’s paired in-process validation is valid candidate-selection evidence because it compares the authored and accepted values on identical chunks. It is not persistence evidence. Use the fresh-process replay report to prove that the saved layers compose the selected values and reproduce the expected motion.

Step 13: Compare the stages and choose the candidate#

The independently replayed stages produced:

Held-out metric

Poor seed

Friction candidate

Friction+damping candidate

Normalized cost

0.008066

0.004620

0.003285

Position RMSE

0.004443 rad

0.004073 rad

0.004153 rad

Velocity RMSE

0.017925 rad/s

0.015476 rad/s

0.015644 rad/s

Torque RMSE

2.1922 N*m

1.6573 N*m

1.3946 N*m

Held-out Franka error for the poor seed, friction candidate, and friction-plus-damping candidate.

The damping phase lowers aggregate cost and torque error, while position and velocity regress slightly relative to the friction-only candidate. All values come from independent persisted-stage replays.#

Relative to the friction candidate, the second phase lowers normalized cost by 28.9% and torque RMSE by 15.9%. Position and velocity RMSE increase by 2.0% and 1.1%. It passes the illustrative torque-refinement gate, but it does not strictly dominate the friction candidate.

Choose according to the policy declared in Step 11:

  • For torque-sensitive validation with the stated state-error tolerances, advance the friction+damping candidate to task-level testing.

  • For a strict no-regression policy or position-priority task, retain the friction-only candidate.

  • Preserve both stages. Promotion never requires overwriting the earlier accepted result.

Step 14: Decide whether another parameter phase is justified#

The second-stage report contains useful stopping evidence:

  • No damping scale hit a bound.

  • Parameter-confidence curvature was inconclusive for all seven damping scales.

  • Validation cost remained approximately 8.15 times the estimated noise floor.

  • Joint 2 retained the largest torque-residual power in the report.

  • Training and validation are still two segments of one multisine recording.

Do not respond by opening armature, link mass, center of mass, and inertia together on the same data. These families can explain overlapping acceleration-dependent residuals. Instead, collect independently designed motions: slow or static configurations for gravity terms, acceleration-rich segments for inertial terms, and a separate set of held-out frequencies. Then repeat the same cycle for one supported family at a time:

  1. Establish the persisted-stage baseline.

  2. Run the check and local sensitivity analysis.

  3. Solve without --save-stage.

  4. Apply the reviewed selected status to a new USD.

  5. Replay the new stage in a fresh process.

  6. Compare against the predeclared gate and retain or reject the stage.

For a separate known-truth algorithm and backend check, run Tutorial 15: Validate System Identification with Sim-to-Sim Recovery. That tutorial uses a simple double pendulum and transfers a Newton-recovered model into an NVIDIA Isaac Sim PhysX stage. It complements this recorded-data walkthrough; it does not turn the Franka fit into a known-truth result.

Step 15: Preserve evidence and decide#

Keep:

  • Source, poor-seed, and every candidate USD.

  • Recording, topic mapping, collection manifest, and declared train/validation split.

  • One exported RunSpec per parameter phase.

  • Baseline, sensitivity, result, checkpoint, apply summary, and independent replay reports.

  • Validation figures, acceptance policy, and any failed diagnostic reports.

  • Source revision, extension version, input hashes, GPU, driver, and optimizer seed.

The selected candidate is suitable for additional task validation only under the policy you declared before its solve. Keep the source USD untouched, and do not treat either tutorial outcome as a hardware-safety or physical-parameter certification.

Troubleshooting#

Gravity feedforward does not support joint type …

The message names the Newton joint type and its model joint index, counted from 0 at the root joint in articulation order. Feedforward supports revolute, prismatic, and fixed joints on a fixed base. Newton imports a fully locked generic USD joint as a D6 joint with no free coordinates, which is treated as fixed. A FREE root means the robot is floating-base, and BALL or partly locked D6 joints are not supported. Fix the named joint on the stage, or set Controller feedforward (simulation.newton.feedforward) to none for that articulation.

Newton MuJoCo rollout diverged or cudaErrorIllegalAddress (CUDA error 700)

MuJoCo integrates drive stiffness explicitly. When a drive is too stiff for the inertia it moves, the rollout runs away within a few samples. Each candidate world is checked after every physics step; a diverged world is reset to its initial state and scored as an invalid candidate. If every world in a batch diverges, the rollout fails, and the message reports the stiffest drive, its joint path, and a minimum simulation.newton.mujoco_substeps (MuJoCo substeps in the panel). Before raising the substep count, check the asset itself. USD angular drive stiffness is per degree, so a gain meant per radian is about 57 times too stiff. Implausibly small link inertias and missing joint armature lower the stable step as well. A CUDA error 700 with an older build has the same cause.

A headless tool printed an error but the shell reported success

Older builds discarded the tool’s exit status when shutting down NVIDIA Isaac Sim. The headless SysID tools now return a nonzero status when the run fails, so scripts can rely on the exit code.

Summary#

This tutorial covered:

  1. Creating a controlled over-frictional working asset without modifying the source.

  2. Declaring link-side torque semantics and correcting a measured 0.5361 s recording offset.

  3. Separating coupled-multisine training and held-out frequency content.

  4. Solving and independently replaying a bounded seven-friction phase.

  5. Diagnosing the persisted residual before opening a second parameter family.

  6. Solving and independently replaying a bounded seven-damping phase.

  7. Comparing both candidates with an explicit application tradeoff.

  8. Stopping when the existing excitation no longer supports a defensible broader solve.

  9. Separating predictive fit quality from physical identifiability and known-truth validation.

Back to Gallery View