Add SO3 transmission and native orientation actuator.
https://youtu.be/17XpwnqyCXs New transmission type mjTRN_SO3: a relative orientation, targeting a ball joint or a site+refsite pair. It is the first transmission with more than one force output: its length is the norm of the expmap vector of the relative rotation and its moment axes are the 3 rows of the relative rotational Jacobian, without projecting onto per-actuator gears. New force law mjGAIN_SO3/mjBIAS_SO3: a geodesic PD servo, force = kp * log(q_current^-1 * q_target) - kv * velocity, exact for arbitrary axis combinations with a unique equilibrium at every commanded orientation. Error, moment rows and velocity all live in the child frame (joint or site): the right-difference error is the gradient of the geodesic potential in that frame. The parent-frame (left) error is not: driving child-frame torques with it pumps energy at large angles, settling into steady-spinning limit cycles (the SO3LargeAngleConvergence test). The integrator variant stores the 3D orientation setpoint in act (actnum = 3, re-anchored to a bounded representative at integration time). Exposed in MJCF as <orientation joint=|site=+refsite= kp kv|dampratio>, or via <general gaintype="so3" biastype="so3">. The setpoint input has two charts: an expmap target (3 controls, default) or a quaternion target (4 controls) -- <orientation input="quat">, the first actuator with different input and output widths. The signature is recorded in a new per-actuator field actuator_ctrlspec (mjtCtrlChart), whose meaning is scoped by the gain type the way gain/bias parameters are; ctrlnum is derived from it at compile time and remains the layout authority. An explicit field rather than width inference or a prm slot: width-as-chart cannot express same-width signatures (upcoming servo input subsets), and prm slots are the input_mode pattern this stack retires. The force law normalizes the commanded quaternion, making it scale- and antipodally-invariant. The all-zero ctrl still maps to the identity via mju_normalize4, but it is a degenerate point (a nudge of any component commands a half-turn), so quat inputs reset to the identity quaternion: new mj_resetCtrl sets neutral ctrl values (zero, except qw = 1), called by mj_resetData and the viewers' Clear All. The quat chart is restricted to dyntype 'none': integrating a quaternion setpoint linearly is not meaningful on the manifold. New mjsActuator.ctrlspec field carries the signature through the spec and XML round-trip. Actuator sensors (actuatorpos/vel/frc) now report one value per force output; dim = 3 on an SO3 actuator. As the first actuator with nu != nactuator, this commit also makes the viewers multi-input aware: the control sliders in simulate and studio, which indexed per-actuator arrays by control index (out of bounds on this model class), are generated per control and labeled with the actuator name plus an input suffix ("orient/qw"), via the new introspection helper mj_actuatorInputName -- the single source of truth for input names, extended by each new multi-input type (quaternion components are w-first: qw, qx, qy, qz). Slider ranges now honor a defined ctrlrange even when ctrllimited is false: range is the UI hint, limited is the clamp -- wrapped and expmap setpoints are unbounded but still want finite sliders, while quat components are truly bounded. The rotational demo model is orientation.xml under test/engine/testdata/actuation/, upgraded to a three-way contrast: per-axis wrapped servos vs an expmap-commanded vs a quat-commanded orientation actuator, on identical checker-textured boxes. It is loaded by the mixed-axis contrast and input-name tests, and doubles as the viewer test model (slider groups of 3 independent, 3 grouped, 4 grouped). PiperOrigin-RevId: 951607063 Change-Id: If235dba8e2f2ca72672e7c62531a27e967c6a373
This commit is contained in:
committed by
Copybara-Service
parent
a8545ac7cc
commit
072e963fa0
@@ -166,6 +166,7 @@ PYBIND11_MODULE(_functions, pymodule, pybind11::mod_gil_not_used()) {
|
||||
InterceptMjErrors(::mj_copyData)(dest, m, src);
|
||||
});
|
||||
Def<traits::mj_resetData>(pymodule);
|
||||
Def<traits::mj_resetCtrl>(pymodule);
|
||||
Def<traits::mj_resetDataDebug>(pymodule);
|
||||
Def<traits::mj_resetDataKeyframe>(pymodule);
|
||||
// Skipped: mj_stackAllocByte (doesn't make sense in Python)
|
||||
@@ -589,6 +590,7 @@ PYBIND11_MODULE(_functions, pymodule, pybind11::mod_gil_not_used()) {
|
||||
});
|
||||
Def<traits::mj_name2id>(pymodule);
|
||||
Def<traits::mj_id2name>(pymodule);
|
||||
Def<traits::mj_actuatorInputName>(pymodule);
|
||||
Def<traits::mj_fullM>(
|
||||
pymodule,
|
||||
[](const raw::MjModel* m, const raw::MjData* d,
|
||||
|
||||
@@ -251,6 +251,7 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjTRN_TENDON', 3),
|
||||
('mjTRN_SITE', 4),
|
||||
('mjTRN_BODY', 5),
|
||||
('mjTRN_SO3', 6),
|
||||
('mjTRN_UNDEFINED', 1000),
|
||||
]),
|
||||
)),
|
||||
@@ -277,7 +278,8 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjGAIN_AFFINE', 1),
|
||||
('mjGAIN_MUSCLE', 2),
|
||||
('mjGAIN_DCMOTOR', 3),
|
||||
('mjGAIN_USER', 4),
|
||||
('mjGAIN_SO3', 4),
|
||||
('mjGAIN_USER', 5),
|
||||
]),
|
||||
)),
|
||||
('mjtBias',
|
||||
@@ -289,7 +291,17 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjBIAS_AFFINE', 1),
|
||||
('mjBIAS_MUSCLE', 2),
|
||||
('mjBIAS_DCMOTOR', 3),
|
||||
('mjBIAS_USER', 4),
|
||||
('mjBIAS_SO3', 4),
|
||||
('mjBIAS_USER', 5),
|
||||
]),
|
||||
)),
|
||||
('mjtCtrlChart',
|
||||
EnumDecl(
|
||||
name='mjtCtrlChart',
|
||||
declname='enum mjtCtrlChart',
|
||||
values=dict([
|
||||
('mjCHART_EXPMAP', 1),
|
||||
('mjCHART_QUAT', 2),
|
||||
]),
|
||||
)),
|
||||
('mjtObj',
|
||||
|
||||
@@ -1102,6 +1102,26 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Copy mjData, skip large arrays not required for visualization.',
|
||||
)),
|
||||
('mj_resetCtrl',
|
||||
FunctionDecl(
|
||||
name='mj_resetCtrl',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='m',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjModel', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='d',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjData'),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Reset ctrl to neutral values: zero, except quaternion inputs which reset to the identity.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mj_resetData',
|
||||
FunctionDecl(
|
||||
name='mj_resetData',
|
||||
@@ -3467,6 +3487,30 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Get name of object with the specified mjtObj type and id; return NULL if name not found.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mj_actuatorInputName',
|
||||
FunctionDecl(
|
||||
name='mj_actuatorInputName',
|
||||
return_type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='m',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjModel', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='id',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='input',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
),
|
||||
doc='Get name of actuator input, determined by the actuator type and input signature; return NULL if the actuator type defines no input names.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mj_fullM',
|
||||
FunctionDecl(
|
||||
name='mj_fullM',
|
||||
@@ -10927,6 +10971,44 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Set actuator to velocity servo; return error if any.',
|
||||
)),
|
||||
('mjs_setToOrientation',
|
||||
FunctionDecl(
|
||||
name='mjs_setToOrientation',
|
||||
return_type=PointerType(
|
||||
inner_type=ValueType(name='char', is_const=True),
|
||||
),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='actuator',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjsActuator'),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='kp',
|
||||
type=ValueType(name='double'),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='kv',
|
||||
type=ArrayType(
|
||||
inner_type=ValueType(name='double'),
|
||||
extents=(1,),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='dampratio',
|
||||
type=ArrayType(
|
||||
inner_type=ValueType(name='double'),
|
||||
extents=(1,),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='ctrlspec',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
),
|
||||
doc='Set actuator to orientation servo.',
|
||||
)),
|
||||
('mjs_setToDamper',
|
||||
FunctionDecl(
|
||||
name='mjs_setToDamper',
|
||||
|
||||
@@ -4452,6 +4452,14 @@ STRUCTS: Mapping[str, StructDecl] = dict([
|
||||
doc='number of controls',
|
||||
array_extent=('nactuator',),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_ctrlspec',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='int'),
|
||||
),
|
||||
doc='input signature, scoped by gaintype',
|
||||
array_extent=('nactuator',),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_outadr',
|
||||
type=PointerType(
|
||||
@@ -4620,6 +4628,22 @@ STRUCTS: Mapping[str, StructDecl] = dict([
|
||||
doc='plugin instance id; -1: not a plugin',
|
||||
array_extent=('nactuator',),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_forcelimited',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtBool'),
|
||||
),
|
||||
doc='is force limited',
|
||||
array_extent=('nactuator',),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_forcerange',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtNum'),
|
||||
),
|
||||
doc='range of forces',
|
||||
array_extent=('nactuator', 2),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_ctrllimited',
|
||||
type=PointerType(
|
||||
@@ -4644,22 +4668,6 @@ STRUCTS: Mapping[str, StructDecl] = dict([
|
||||
doc='scale length and transmitted force',
|
||||
array_extent=('nout', 6),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_forcelimited',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtBool'),
|
||||
),
|
||||
doc='is force limited',
|
||||
array_extent=('nout',),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_forcerange',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtNum'),
|
||||
),
|
||||
doc='range of forces',
|
||||
array_extent=('nout', 2),
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actuator_acc0',
|
||||
type=PointerType(
|
||||
@@ -9658,6 +9666,11 @@ STRUCTS: Mapping[str, StructDecl] = dict([
|
||||
type=ValueType(name='int'),
|
||||
doc='number of activation variables',
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='ctrlspec',
|
||||
type=ValueType(name='int'),
|
||||
doc='input signature, scoped by gaintype; 0: type default',
|
||||
),
|
||||
StructFieldDecl(
|
||||
name='actearly',
|
||||
type=ValueType(name='mjtBool'),
|
||||
|
||||
@@ -1554,6 +1554,19 @@ PYBIND11_MODULE(_specs, m, pybind11::mod_gil_not_used()) {
|
||||
}
|
||||
},
|
||||
py::arg("kv"));
|
||||
mjsActuator.def(
|
||||
"set_to_orientation",
|
||||
[](raw::MjsActuator* self, double kp, double kv, double dampratio,
|
||||
int ctrlspec) {
|
||||
std::string err = mjs_setToOrientation(
|
||||
self, kp, kv == -1 ? nullptr : &kv,
|
||||
dampratio == -1 ? nullptr : &dampratio, ctrlspec);
|
||||
if (!err.empty()) {
|
||||
throw pybind11::value_error(err);
|
||||
}
|
||||
},
|
||||
py::arg("kp"), py::arg("kv") = -1, py::arg("dampratio") = -1,
|
||||
py::arg("ctrlspec") = 0);
|
||||
mjsActuator.def(
|
||||
"set_to_damper",
|
||||
[](raw::MjsActuator* self, double kv) {
|
||||
|
||||
@@ -1588,6 +1588,14 @@ class SpecsTest(absltest.TestCase):
|
||||
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_AFFINE)
|
||||
self.assertEqual(actuator.inheritrange, True)
|
||||
|
||||
actuator.set_to_orientation(kp=2.0, dampratio=1.0)
|
||||
self.assertEqual(actuator.gainprm[0], 2)
|
||||
self.assertEqual(actuator.biasprm[1], -2)
|
||||
self.assertEqual(actuator.biasprm[2], 1)
|
||||
self.assertEqual(actuator.gaintype, mujoco.mjtGain.mjGAIN_SO3)
|
||||
self.assertEqual(actuator.biastype, mujoco.mjtBias.mjBIAS_SO3)
|
||||
self.assertEqual(actuator.dyntype, mujoco.mjtDyn.mjDYN_NONE)
|
||||
|
||||
actuator.set_to_velocity(kv=5.0)
|
||||
self.assertEqual(actuator.gainprm[0], 5)
|
||||
self.assertEqual(actuator.biasprm[2], -5)
|
||||
|
||||
Reference in New Issue
Block a user