From 382474bb9dd1bd5fa248cba28bf965c5854e74c5 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 7 Apr 2026 08:10:42 -0700 Subject: [PATCH] Minor improvements to `dcmotor` PiperOrigin-RevId: 895907301 Change-Id: Ia50a6d06c1ede9894cc71f375db837d104f0211e --- doc/APIreference/functions.rst | 2 ++ doc/XMLreference.rst | 46 ++++++++++++------------ include/mujoco/mujoco.h | 1 + python/mujoco/introspect/functions.py | 8 +++++ src/user/user_api.cc | 50 +++++++++++++-------------- src/user/user_api.h | 7 ++++ test/user/user_api_test.cc | 41 ++++++++++++++++++++++ wasm/codegen/generated/bindings.cc | 16 ++++----- 8 files changed, 114 insertions(+), 57 deletions(-) diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index fa61555b..ef49bf39 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -4667,6 +4667,8 @@ Set actuator to active adhesion; return error if any. Set actuator to DC motor; return error if any. +*Nullable:* ``motorconst``, ``nominal``, ``saturation``, ``inductance``, ``cogging``, ``controller``, ``thermal``, ``lugre`` + .. _AddAssets: Assets diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 2552f932..72bfd3d8 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -6327,17 +6327,16 @@ This element has a subset of the common attributes and two custom attributes. :el-prefix:`actuator/` |-| **dcmotor** |*| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This element creates a DC motor actuator. Note that :el:`dcmotor` is quite different from the :ref:`general actuation -model`. Unlike the general model where the components of force generation are independent affine functions -mapping from control to force, :el:`dcmotor` relies on highly coupled physical dynamics. See the `DC motor technical -note <_static/dcmotor.pdf>`__ for complete mathematical formulations and parameter semantics, but we include a few -important notes here: +This element creates a DC motor actuator. See the `DC motor technical note <_static/dcmotor.pdf>`__ for complete +mathematical formulations and parameter semantics, but we include a few important notes below. Note that :el:`dcmotor` +does not conform to the affine gain / bias structure of the :ref:`general actuation model`, except for +the stateless case. -- Note that while :ref:`resistance`, :ref:`motorconst` and - :ref:`nominal` are each optional, some combination of them is required. +- :ref:`resistance`, :ref:`motorconst` and + :ref:`nominal` are each optional, but some combination of them is required. See Section 2.1 of the `technical note <_static/dcmotor.pdf>`__. -- The control :ref:`input` semantic is either the voltage applied to the motor terminals, or a - position or velocity target for a PID :ref:`controller`. +- The control :ref:`input` semantic is either the voltage applied to the motor terminals (the + default), or a position or velocity target for a :ref:`PID controller`. - Optional features include electrical dynamics (:ref:`inductance`), :ref:`cogging torque`, :ref:`thermal resistance variation`, and :ref:`LuGre` friction. @@ -6408,7 +6407,7 @@ This element has the following custom attributes in addition to the common attri .. _actuator-dcmotor-resistance: :at:`resistance`: :at-val:`real, optional` - Terminal resistance :math:`R` in Ohm. (see `tech note <_static/dcmotor.pdf>`__ for details) + Terminal resistance :math:`R` in Ohm. (see `tech note <_static/dcmotor.pdf>`__, Sections 1.1 and 2.1) .. _actuator-dcmotor-motorconst: @@ -6416,16 +6415,15 @@ This element has the following custom attributes in addition to the common attri Motor constants, defined as :at:`motorconst` = ":at-val:`Kt` :at-val:`Ke`" (N·m/A, equivalently V·s/rad). :at-val:`Kt` is the torque constant and :at-val:`Ke` the back-EMF constant; they can differ when magnetic saturation is present. If both are positive, the effective constant is :math:`K = \sqrt{K_t K_e}` (geometric mean). If only one - is positive, :math:`K` equals that value; a single value is interpreted as :math:`K_t = K_e`. If your datasheet gives - the speed constant :math:`K_v` in rad/(V·s), use :math:`K_e = 1/K_v`. (see `tech note <_static/dcmotor.pdf>`__ for - details) + is positive, :math:`K` equals that value. If a datasheet specifies the speed constant :math:`K_v` in rad/(V·s), use + :math:`K_e = 1/K_v`. (see `tech note <_static/dcmotor.pdf>`__, Sections 1.1 and 2.1) .. _actuator-dcmotor-nominal: :at:`nominal`: :at-val:`real(3), optional` Nominal operating point, defined as :at:`nominal` = ":at-val:`voltage` :at-val:`stall_torque` :at-val:`no_load_speed`". The compiler derives :math:`K =` :at-val:`voltage` / :at-val:`no_load_speed` and :math:`R = - K` · :at-val:`voltage` / :at-val:`stall_torque`. (see `tech note <_static/dcmotor.pdf>`__ for details) + K` · :at-val:`voltage` / :at-val:`stall_torque`. (see `tech note <_static/dcmotor.pdf>`__, Sections 1.1 and 2.1) .. _actuator-dcmotor-inductance: @@ -6434,7 +6432,7 @@ This element has the following custom attributes in addition to the common attri alternative specifications: :at-val:`L` is the winding inductance and :at-val:`timeconst` :math:`= L/R` is the electrical time constant. Specify one; if both are given, :at-val:`L` takes precedence. If both are 0 (the default), no electrical dynamics are modeled and the current is computed algebraically. Adds one activation variable for - armature current. (see `tech note <_static/dcmotor.pdf>`__ for details) + armature current. (see `tech note <_static/dcmotor.pdf>`__, Sections 1.1.1 and 2.2) .. _actuator-dcmotor-thermal: @@ -6444,7 +6442,7 @@ This element has the following custom attributes in addition to the common attri specify the thermal time constant: :at-val:`timeconst` = :at-val:`resistance` :math:`\times` :at-val:`capacitance`. Specify either :at-val:`timeconst` directly, or :at-val:`resistance` and :at-val:`capacitance`; if all three are given, :at-val:`timeconst` takes precedence. If all are 0 (the default), thermal modeling is disabled. Adds one - activation variable for winding temperature. (see `tech note <_static/dcmotor.pdf>`__ for details) + activation variable for winding temperature. (see `tech note <_static/dcmotor.pdf>`__, Sections 1.3 and 2.3) .. _actuator-dcmotor-saturation: @@ -6455,7 +6453,7 @@ This element has the following custom attributes in addition to the common attri given, :at-val:`torque` takes precedence. Sets :at:`forcerange` to [:math:`-\tau_{\max},\, \tau_{\max}`]. :at-val:`voltage` sets the maximum voltage :math:`V_{\max}`. :at-val:`current_rate` sets the maximum rate of change of current :math:`(di/dt)_{\max}` (requires :ref:`inductance`). A value of 0 (the - default) for any sub-value disables the respective limit. (see `tech note <_static/dcmotor.pdf>`__ for details) + default) for any sub-value disables the respective limit. (see `tech note <_static/dcmotor.pdf>`__, Section 2) .. _actuator-dcmotor-cogging: @@ -6463,7 +6461,7 @@ This element has the following custom attributes in addition to the common attri Cogging torque, defined as :at:`cogging` = ":at-val:`amplitude` :at-val:`poles` :at-val:`phase`" (N·m, integer, rad). Adds a position-dependent torque :math:`= \textsf{amplitude} \cdot \sin(\textsf{poles} \cdot \theta + \textsf{phase})`. Disabled when :at-val:`amplitude` = 0 (the default). - (see `tech note <_static/dcmotor.pdf>`__ for details) + (see `tech note <_static/dcmotor.pdf>`__, Sections 1.2 and 2.1) .. _actuator-dcmotor-lugre: @@ -6473,28 +6471,28 @@ This element has the following custom attributes in addition to the common attri :at-val:`stiffness` = 0 (the default). Adds one activation variable for bristle deflection. Note that the :at-val:`viscous` coefficient is mapped directly to the actuator :ref:`damping` array (specifically the linear term, :at-val:`damping[0]`). If both are specified, their values are summed. - (see `tech note <_static/dcmotor.pdf>`__ for details) + (see `tech note <_static/dcmotor.pdf>`__, Sections 1.4 and 2.4) .. _actuator-dcmotor-input: :at:`input`: :at-val:`[voltage, position, velocity], "voltage"` Specifies the input signal semantics. In "voltage" mode, the control directly sets applied motor voltage. In - "position" or "velocity" modes, the PID :ref:`controller` uses the control as a - reference setpoint relative to the joint trajectory. (see `tech note <_static/dcmotor.pdf>`__ for details) + "position" or "velocity" modes, the :ref:`PID controller` uses the control as a + reference setpoint relative to the joint trajectory. (see `tech note <_static/dcmotor.pdf>`__, Section 2.5) .. _actuator-dcmotor-controller: :at:`controller`: :at-val:`real(5), "0 0 0 0 0"` PID controller parameters, defined as :at:`controller` = ":at-val:`kp` :at-val:`ki` :at-val:`kd` :at-val:`slewmax` :at-val:`Imax`". Depending on the :at:`input` mode, the controller stabilizes either position or - velocity. If the :at:`input` mode is voltage, the controller is ignored. A value of 0 (the default) disables the + velocity. If the :at:`input` mode is voltage, this attribute is ignored. A value of 0 (the default) disables the respective feature: :at-val:`slewmax` = 0 means no slew-rate limiting, :at-val:`Imax` = 0 means no anti-windup - clamping. (see `tech note <_static/dcmotor.pdf>`__ for details) + clamping. (see `tech note <_static/dcmotor.pdf>`__, Section 2.5) .. _actuator-plugin: :el-prefix:`actuator/` |-| **plugin** |?| -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Associate this actuator with an :ref:`engine plugin`. Either :at:`plugin` or :at:`instance` are required. diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index ba496360..e8bbe692 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1727,6 +1727,7 @@ MJAPI const char* mjs_setToMuscle(mjsActuator* actuator, double timeconst[2], do MJAPI const char* mjs_setToAdhesion(mjsActuator* actuator, double gain); // Set actuator to DC motor; return error if any. +// Nullable: motorconst, nominal, saturation, inductance, cogging, controller, thermal, lugre MJAPI const char* mjs_setToDCMotor(mjsActuator* actuator, double motorconst[2], double resistance, double nominal[3], double saturation[4], double inductance[2], double cogging[3], double controller[5], double thermal[6], diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 0ab6df20..262d3430 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -10805,6 +10805,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(2,), ), + nullable=True, ), FunctionParameterDecl( name='resistance', @@ -10816,6 +10817,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(3,), ), + nullable=True, ), FunctionParameterDecl( name='saturation', @@ -10823,6 +10825,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(4,), ), + nullable=True, ), FunctionParameterDecl( name='inductance', @@ -10830,6 +10833,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(2,), ), + nullable=True, ), FunctionParameterDecl( name='cogging', @@ -10837,6 +10841,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(3,), ), + nullable=True, ), FunctionParameterDecl( name='controller', @@ -10844,6 +10849,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(5,), ), + nullable=True, ), FunctionParameterDecl( name='thermal', @@ -10851,6 +10857,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(6,), ), + nullable=True, ), FunctionParameterDecl( name='lugre', @@ -10858,6 +10865,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ inner_type=ValueType(name='double'), extents=(6,), ), + nullable=True, ), FunctionParameterDecl( name='input_mode', diff --git a/src/user/user_api.cc b/src/user/user_api.cc index 2317cc3a..8d320463 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -1125,18 +1125,18 @@ const char* mjs_setToDCMotor(mjsActuator* actuator, double motorconst[2], double double nominal[3], double saturation[4], double inductance[2], double cogging[3], double controller[5], double thermal[6], double lugre[6], int input_mode) { - double Kt = motorconst[0]; // torque constant - double Ke = motorconst[1]; // back-EMF constant - double R = resistance; // electrical resistance - double vn = nominal[0]; // nominal voltage - double tau0 = nominal[1]; // stall torque - double omega0 = nominal[2]; // no-load speed + double R = resistance; // electrical resistance + double Kt = motorconst ? motorconst[0] : 0; // torque constant + double Ke = motorconst ? motorconst[1] : 0; // back-EMF constant + double vn = nominal ? nominal[0] : 0; // nominal voltage + double tau0 = nominal ? nominal[1] : 0; // stall torque + double omega0 = nominal ? nominal[2] : 0; // no-load speed // derive Ke from nominal: omega0 = vn*Ke / (Ke^2 + R*B) if (vn > 0 && Ke <= 0 && omega0 > 0) { // viscous damping (linear), add lugre sigma2 contribution if any double B = actuator->damping[0]; - if (lugre[0] > 0) B += lugre[2]; + if (lugre && lugre[0] > 0) B += lugre[2]; if (B > 0 && R > 0) { // R known: solve quadratic Ke^2*omega0 - Ke*vn + R*B*omega0 = 0 @@ -1176,24 +1176,24 @@ const char* mjs_setToDCMotor(mjsActuator* actuator, double motorconst[2], double actuator->gainprm[1] = K; // controller parameters: gainprm[4:6] for kp, ki, kd - actuator->gainprm[4] = controller[0]; // kp - actuator->gainprm[5] = controller[1]; // ki - actuator->gainprm[6] = controller[2]; // kd + actuator->gainprm[4] = controller ? controller[0] : 0; // kp + actuator->gainprm[5] = controller ? controller[1] : 0; // ki + actuator->gainprm[6] = controller ? controller[2] : 0; // kd // controller parameters: dynprm[7,8] for slewmax, Imax - actuator->dynprm[7] = controller[3]; // slewmax - actuator->dynprm[8] = controller[4]; // Imax + actuator->dynprm[7] = controller ? controller[3] : 0; // slewmax + actuator->dynprm[8] = controller ? controller[4] : 0; // Imax // saturation: [tau_max, i_max, (di/dt)_max, v_max] - if (saturation[2] > 0) { + if (saturation && saturation[2] > 0) { actuator->dynprm[1] = saturation[2]; // (di/dt)_max } - if (saturation[3] > 0) { + if (saturation && saturation[3] > 0) { actuator->gainprm[7] = saturation[3]; // v_max } // saturation -> forcerange - if (saturation[0] > 0 || saturation[1] > 0) { + if (saturation && (saturation[0] > 0 || saturation[1] > 0)) { double tau_max = saturation[0]; if (tau_max == 0 && saturation[1] > 0) { tau_max = K * saturation[1]; // tau_max = K * i_max @@ -1204,34 +1204,34 @@ const char* mjs_setToDCMotor(mjsActuator* actuator, double motorconst[2], double } // cogging: [amplitude, periodicity, phase] -> biasprm[0:3] - actuator->biasprm[0] = cogging[0]; // amplitude - actuator->biasprm[1] = cogging[1]; // periodicity - actuator->biasprm[2] = cogging[2]; // phase + actuator->biasprm[0] = cogging ? cogging[0] : 0; // amplitude + actuator->biasprm[1] = cogging ? cogging[1] : 0; // periodicity + actuator->biasprm[2] = cogging ? cogging[2] : 0; // phase // count activation variables: slot order is slew, integral, temperature, bristle, current int actdim = 0; // inductance: [L, te] - if (inductance[0] < 0) return "DC motor: inductance must be non-negative"; - if (inductance[1] < 0) return "DC motor: electrical time constant must be non-negative"; - double te = inductance[0] > 0 ? inductance[0] / R : inductance[1]; + if (inductance && inductance[0] < 0) return "DC motor: inductance must be non-negative"; + if (inductance && inductance[1] < 0) return "DC motor: electrical time constant must be non-negative"; + double te = (inductance && inductance[0] > 0) ? inductance[0] / R : (inductance ? inductance[1] : 0); actuator->dynprm[0] = te; if (te > 0) { actdim++; } // controller states: slew rate limiting - if (controller[3] > 0) { // slewmax + if (controller && controller[3] > 0) { // slewmax actdim++; } // controller states: integral - if (controller[1] > 0) { // ki + if (controller && controller[1] > 0) { // ki actdim++; } // thermal -> temperature activation - if (thermal[0] > 0 || thermal[1] > 0 || thermal[2] > 0) { + if (thermal && (thermal[0] > 0 || thermal[1] > 0 || thermal[2] > 0)) { double RT = thermal[0]; // thermal resistance double C = thermal[1]; // thermal capacitance double tth = thermal[2]; // thermal time constant @@ -1259,7 +1259,7 @@ const char* mjs_setToDCMotor(mjsActuator* actuator, double motorconst[2], double } // lugre: {stiffness, damping, viscous, coulomb, static, stribeck} - if (lugre[0] > 0) { + if (lugre && lugre[0] > 0) { actuator->dynprm[5] = lugre[0]; // stiffness -> sigma0 actuator->dynprm[6] = lugre[1]; // damping -> sigma1 actuator->damping[0] += lugre[2]; // viscous -> sigma2 diff --git a/src/user/user_api.h b/src/user/user_api.h index 9bae6bdd..307f1a53 100644 --- a/src/user/user_api.h +++ b/src/user/user_api.h @@ -192,6 +192,13 @@ MJAPI const char* mjs_setToMuscle(mjsActuator* actuator, double timeconst[2], do // Set actuator to adhesion, return error on failure. MJAPI const char* mjs_setToAdhesion(mjsActuator* actuator, double gain); +// Set actuator to DC motor, return error on failure. +MJAPI const char* mjs_setToDCMotor(mjsActuator* actuator, double motorconst[2], double resistance, + double nominal[3], double saturation[4], double inductance[2], + double cogging[3], double controller[5], double thermal[6], + double lugre[6], int input_mode); + + //---------------------------------- Add assets ---------------------------------------------------- // Add mesh. diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index eb8a28b2..b36557aa 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -239,6 +239,47 @@ TEST_F(MujocoTest, DeletePlugin) { mj_deleteModel(newmodel); } +TEST_F(MujocoTest, SetToDCMotorNullable) { + mjSpec* spec = mj_makeSpec(); + mjsActuator* actuator = mjs_addActuator(spec, 0); + + double motorconst[2] = {0.05, 0.05}; + double resistance = 2.0; + + const char* err = mjs_setToDCMotor(actuator, motorconst, resistance, + nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, + nullptr, 0); + EXPECT_STREQ(err, ""); + EXPECT_EQ(actuator->gainprm[0], 2.0); + EXPECT_EQ(actuator->gainprm[1], 0.05); + EXPECT_EQ(actuator->gainprm[4], 0); + EXPECT_EQ(actuator->gainprm[5], 0); + EXPECT_EQ(actuator->gainprm[6], 0); + EXPECT_EQ(actuator->dynprm[7], 0); + EXPECT_EQ(actuator->dynprm[8], 0); + + mj_deleteSpec(spec); +} + +TEST_F(MujocoTest, SetToDCMotorDeriveKe) { + mjSpec* spec = mj_makeSpec(); + mjsActuator* actuator = mjs_addActuator(spec, 0); + + double resistance = 2.0; + double nominal[3] = {12.0, 0, 100.0}; // vn=12, omega0=100 + + const char* err = mjs_setToDCMotor(actuator, nullptr, resistance, + nominal, nullptr, nullptr, + nullptr, nullptr, nullptr, + nullptr, 0); + EXPECT_STREQ(err, ""); + EXPECT_EQ(actuator->gainprm[0], 2.0); + EXPECT_NEAR(actuator->gainprm[1], 0.12, 1e-5); + + mj_deleteSpec(spec); +} + static constexpr char xml_plugin_1[] = R"( diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index cfa83b40..dc2f2fc9 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -9877,14 +9877,14 @@ std::string mjs_setToCylinder_wrapper(MjsActuator& actuator, double timeconst, d } std::string mjs_setToDCMotor_wrapper(MjsActuator& actuator, const val& motorconst, double resistance, const val& nominal, const val& saturation, const val& inductance, const val& cogging, const val& controller, const val& thermal, const val& lugre, int input_mode) { - UNPACK_VALUE(double, motorconst); - UNPACK_VALUE(double, nominal); - UNPACK_VALUE(double, saturation); - UNPACK_VALUE(double, inductance); - UNPACK_VALUE(double, cogging); - UNPACK_VALUE(double, controller); - UNPACK_VALUE(double, thermal); - UNPACK_VALUE(double, lugre); + UNPACK_NULLABLE_VALUE(double, motorconst); + UNPACK_NULLABLE_VALUE(double, nominal); + UNPACK_NULLABLE_VALUE(double, saturation); + UNPACK_NULLABLE_VALUE(double, inductance); + UNPACK_NULLABLE_VALUE(double, cogging); + UNPACK_NULLABLE_VALUE(double, controller); + UNPACK_NULLABLE_VALUE(double, thermal); + UNPACK_NULLABLE_VALUE(double, lugre); return std::string(mjs_setToDCMotor(actuator.get(), motorconst_.data(), resistance, nominal_.data(), saturation_.data(), inductance_.data(), cogging_.data(), controller_.data(), thermal_.data(), lugre_.data(), input_mode)); }