Mass and inertia

Change what the robot weighs, and confirm it by watching the robot move.

cargo run -p vrobots-examples --bin ex22_physical_params -- 1
./target/cpp-build/ex22_physical_params 1
python examples/python/ex22_physical_params.py 1
cargo run -p vrobots-examples --bin ex22_physical_params
./target/cpp-build/ex22_physical_params
python examples/python/ex22_physical_params.py

ex22_physical_params takes an optional sys_id. With it, the example attaches to the scene's own multirotor; without it, the example creates one and both climb runs read 0.00 m/s, because a client-created multirotor does not integrate physics in simulator v3.0.0 (known issue). Prefer the argument.

The request

srv/params carries two numbers and is the only channel for either.

FieldTypeUnitsDefaultNotes
massOption<f64>kgNone, meaning untouchedframe-invariant; must be positive and finite
moiOption<[f64;3]>kg·m²None, meaning untouchedprincipal moments, read in your header frame and permuted into the robot's; all three axes must be positive and finite

Build one with PhysicalParams::default() and the with_mass / with_moi setters; is_empty() reports whether the request would change nothing. Moments of inertia are positive quantities, so the frame conversion reorders the triple and never flips a sign.

The service works mid-flight, which is the reason it exists: changing the mass under a running loop is the standard way to test a controller against a payload it was not tuned for.

The confirmation is behavioural

Neither figure appears in the state message. Mass properties are quasi-static configuration, not state, so there is nothing to read back and nothing to diff. The same pulse width has to produce a different acceleration, or the change did not land.

ex22 therefore flies a fixed 1800 us collective twice, at two different masses, and compares the climb rate.

From examples/rust/src/bin/ex22_physical_params.rs:

#![allow(unused)]
fn main() {
    // ===== run 1: light =====
    robot.set_physical_params(&PhysicalParams::default().with_mass(LIGHT_KG).with_moi(MOI))?;
    let light = climb_run(&robot, LIGHT_KG)?;

    // ===== run 2: heavy, same command =====
    robot.set_physical_params(&PhysicalParams::default().with_mass(HEAVY_KG))?;
    let heavy = climb_run(&robot, HEAVY_KG)?;
}
The same in C++ (examples/cpp/ex22_physical_params.cpp)
// ===== run 1: light =====
{
    auto params = vrsdk::physical_params();
    params.has_mass = true;
    params.mass = LIGHT_KG;
    params.has_moi = true;
    params.moi[0] = MOI[0];
    params.moi[1] = MOI[1];
    params.moi[2] = MOI[2];
    robot.set_physical_params(params);
}
const double light = climb_run(robot, LIGHT_KG);

// ===== run 2: heavy, same command =====
{
    auto params = vrsdk::physical_params();
    params.has_mass = true;
    params.mass = HEAVY_KG;
    robot.set_physical_params(params);
}
const double heavy = climb_run(robot, HEAVY_KG);
The same in Python (examples/python/ex22_physical_params.py)
# ===== run 1: light =====
robot.set_physical_params(mass=LIGHT_KG, moi=MOI)
light = climb_run(robot, LIGHT_KG)

# ===== run 2: heavy, same command =====
robot.set_physical_params(mass=HEAVY_KG)
heavy = climb_run(robot, HEAVY_KG)

The three spell "leave this field alone" differently, and it is the pattern for every service in this chapter. Rust chains with_* setters on a default; Python takes keyword arguments and omits what it does not set; C++ builds the plain C struct from vrsdk::physical_params() and sets a has_* flag beside each value. Never write {} in C++ and never forget the has_* flag: the field is then silently not applied.

Same command, same rotors, same air: the heavier aircraft climbs slower, and the gap between the two numbers is the entire receipt.

1800 us on every rotor, 2.0 s of climb, twice:
  1 kg -> <rate> m/s
  2 kg -> <rate> m/s
The difference IS the receipt -- there is no mass field in the state message to read back.

Each run starts with reset(), so the two are comparable. That does not undo the mass: configuration survives a state reset.

Positive only, and refused before it is sent

The simulator does not validate either field. It keeps the prefab's value and acks ok, so a bad request is indistinguishable from a good one from outside.

You sendThe simulator doesWhat you would see
mass <= 0keeps the body's current massnothing
a moment of inertia not strictly positive on all three axeskeeps Unity's collider-derived tensornothing

set_physical_params refuses both cases itself, as VrError::InvalidArgument naming the field, before anything reaches the wire.

#![allow(unused)]
fn main() {
    show_refusal(
        "mass = 0.0",
        robot.set_physical_params(&PhysicalParams::default().with_mass(0.0)),
    );
    show_refusal(
        "moi = [0.02, 0.0, 0.04] (one axis left at zero)",
        robot.set_physical_params(&PhysicalParams::default().with_moi([0.02, 0.0, 0.04])),
    );
    show_refusal(
        "nothing set at all",
        robot.set_physical_params(&PhysicalParams::default()),
    );
}
The same in C++ (examples/cpp/ex22_physical_params.cpp)
show_refusal("mass = 0.0", [&] {
    auto params = vrsdk::physical_params();
    params.has_mass = true;
    params.mass = 0.0;
    robot.set_physical_params(params);
});
show_refusal("moi = [0.02, 0.0, 0.04] (one axis left at zero)", [&] {
    auto params = vrsdk::physical_params();
    params.has_moi = true;
    params.moi[0] = 0.02;
    params.moi[1] = 0.0;
    params.moi[2] = 0.04;
    robot.set_physical_params(params);
});
show_refusal("nothing set at all",
             [&] { robot.set_physical_params(vrsdk::physical_params()); });
The same in Python (examples/python/ex22_physical_params.py)
show_refusal("mass = 0.0", lambda: robot.set_physical_params(mass=0.0))
show_refusal(
    "moi = [0.02, 0.0, 0.04] (one axis left at zero)",
    lambda: robot.set_physical_params(moi=(0.02, 0.0, 0.04)),
)
show_refusal("nothing set at all", lambda: robot.set_physical_params())

The helper takes a callable in C++ and Python because the refusal arrives as a thrown exception, where Rust's takes the returned Result directly. The third case reads differently for the same reason the first two do: an empty request is a bare vrsdk::physical_params() in C++ and a no-argument call in Python.

Each prints the error code and the SDK's explanation instead of a receipt:

-- what the SDK refuses before anything reaches the wire --
  mass = 0.0                                       [<code>] <message>
  moi = [0.02, 0.0, 0.04] (one axis left at zero)  [<code>] <message>
  nothing set at all                               [<code>] <message>
All three are acked `ok` by the simulator and silently ignored, which is why they are caught here instead.

Gotcha. The half-filled inertia triple is the one that catches people: two axes set, the third left at zero. It reads like a partial update and is not. The simulator needs all three strictly positive or it keeps the tensor it already had, so a partial triple changes nothing at all.

The cart pole exception

A CartPole re-stamps its cart's mass from CartPoleConfig::cart_mass on every parameter apply, so a mass sent to srv/params is overwritten a step later on that one robot type. Set it with configure_cartpole instead. Inertia is unaffected.

The defaults are not documented

The prefab mass and inertia a multirotor or a truck spawns with are not recorded anywhere in this repository, and the simulator does not publish them. The only documented behaviour is that a non-positive value leaves them alone.

That matters when you attach rather than create. There is no getter, so the SDK cannot read the old value first and put it back, and reset() will not either. Write down what you started with, or reload the scene when you are done.

Next: Sensor noise

See also: Robot lifecycle, Kinematics, Mass spring damper and cart pole