Add muscle actuators to MJX.
PiperOrigin-RevId: 695334356 Change-Id: I6590ed6bdd4a8adc53d5c4e2641f472f7439cedc
This commit is contained in:
committed by
Copybara-Service
parent
414b677eb8
commit
0f381a9ebd
@@ -6,6 +6,10 @@ Changelog
|
|||||||
Upcoming version (not yet released)
|
Upcoming version (not yet released)
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
MJX
|
||||||
|
^^^
|
||||||
|
- Added muscle actuators.
|
||||||
|
|
||||||
Bug fixes
|
Bug fixes
|
||||||
^^^^^^^^^
|
^^^^^^^^^
|
||||||
- Fixed :github:issue:`2212`, type error in ``mjx.get_data``.
|
- Fixed :github:issue:`2212`, type error in ``mjx.get_data``.
|
||||||
|
|||||||
+3
-9
@@ -214,11 +214,11 @@ The following features are **fully supported** in MJX:
|
|||||||
* - :ref:`Transmission <mjtTrn>`
|
* - :ref:`Transmission <mjtTrn>`
|
||||||
- ``JOINT``, ``JOINTINPARENT``, ``SITE``, ``TENDON``
|
- ``JOINT``, ``JOINTINPARENT``, ``SITE``, ``TENDON``
|
||||||
* - :ref:`Actuator Dynamics <mjtDyn>`
|
* - :ref:`Actuator Dynamics <mjtDyn>`
|
||||||
- ``NONE``, ``INTEGRATOR``, ``FILTER``, ``FILTEREXACT``
|
- ``NONE``, ``INTEGRATOR``, ``FILTER``, ``FILTEREXACT``, ``MUSCLE``
|
||||||
* - :ref:`Actuator Gain <mjtGain>`
|
* - :ref:`Actuator Gain <mjtGain>`
|
||||||
- ``FIXED``, ``AFFINE``
|
- ``FIXED``, ``AFFINE``, ``MUSCLE``
|
||||||
* - :ref:`Actuator Bias <mjtBias>`
|
* - :ref:`Actuator Bias <mjtBias>`
|
||||||
- ``NONE``, ``AFFINE``
|
- ``NONE``, ``AFFINE``, ``MUSCLE``
|
||||||
* - :ref:`Tendon Wrapping <mjtWrap>`
|
* - :ref:`Tendon Wrapping <mjtWrap>`
|
||||||
- ``JOINT``, ``SITE``, ``PULLEY``
|
- ``JOINT``, ``SITE``, ``PULLEY``
|
||||||
* - :ref:`Geom <mjtGeom>`
|
* - :ref:`Geom <mjtGeom>`
|
||||||
@@ -264,12 +264,6 @@ The following features are **in development** and coming soon:
|
|||||||
- ``IMPLICIT``
|
- ``IMPLICIT``
|
||||||
* - Dynamics
|
* - Dynamics
|
||||||
- :ref:`Inverse <mj_inverse>`
|
- :ref:`Inverse <mj_inverse>`
|
||||||
* - :ref:`Actuator Dynamics <mjtDyn>`
|
|
||||||
- ``MUSCLE``
|
|
||||||
* - :ref:`Actuator Gain <mjtGain>`
|
|
||||||
- ``MUSCLE``
|
|
||||||
* - :ref:`Actuator Bias <mjtBias>`
|
|
||||||
- ``MUSCLE``
|
|
||||||
* - :ref:`Tendon Wrapping <mjtWrap>`
|
* - :ref:`Tendon Wrapping <mjtWrap>`
|
||||||
- ``SPHERE``, ``CYLINDER``
|
- ``SPHERE``, ``CYLINDER``
|
||||||
* - Fluid Model
|
* - Fluid Model
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ def fwd_actuation(m: Model, d: Data) -> Data:
|
|||||||
act_dot = ctrl
|
act_dot = ctrl
|
||||||
elif dyn_typ in (DynType.FILTER, DynType.FILTEREXACT):
|
elif dyn_typ in (DynType.FILTER, DynType.FILTEREXACT):
|
||||||
act_dot = (ctrl - act) / jp.clip(dyn_prm[0], mujoco.mjMINVAL)
|
act_dot = (ctrl - act) / jp.clip(dyn_prm[0], mujoco.mjMINVAL)
|
||||||
|
elif dyn_typ == DynType.MUSCLE:
|
||||||
|
act_dot = support.muscle_dynamics(ctrl, act, dyn_prm)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f'dyntype {dyn_typ.name} not implemented.')
|
raise NotImplementedError(f'dyntype {dyn_typ.name} not implemented.')
|
||||||
return act_dot
|
return act_dot
|
||||||
@@ -139,13 +141,15 @@ def fwd_actuation(m: Model, d: Data) -> Data:
|
|||||||
ctrl_act = jp.where(m.actuator_actadr == -1, ctrl, act_last_dim)
|
ctrl_act = jp.where(m.actuator_actadr == -1, ctrl, act_last_dim)
|
||||||
|
|
||||||
def get_force(*args):
|
def get_force(*args):
|
||||||
gain_t, gain_p, bias_t, bias_p, len_, vel, ctrl_act = args
|
gain_t, gain_p, bias_t, bias_p, len_, vel, ctrl_act, len_range, acc0 = args
|
||||||
|
|
||||||
typ, prm = GainType(gain_t), gain_p
|
typ, prm = GainType(gain_t), gain_p
|
||||||
if typ == GainType.FIXED:
|
if typ == GainType.FIXED:
|
||||||
gain = prm[0]
|
gain = prm[0]
|
||||||
elif typ == GainType.AFFINE:
|
elif typ == GainType.AFFINE:
|
||||||
gain = prm[0] + prm[1] * len_ + prm[2] * vel
|
gain = prm[0] + prm[1] * len_ + prm[2] * vel
|
||||||
|
elif typ == GainType.MUSCLE:
|
||||||
|
gain = support.muscle_gain(len_, vel, len_range, acc0, prm)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'unrecognized gaintype {typ.name}.')
|
raise RuntimeError(f'unrecognized gaintype {typ.name}.')
|
||||||
|
|
||||||
@@ -153,13 +157,15 @@ def fwd_actuation(m: Model, d: Data) -> Data:
|
|||||||
bias = jp.array(0.0)
|
bias = jp.array(0.0)
|
||||||
if typ == BiasType.AFFINE:
|
if typ == BiasType.AFFINE:
|
||||||
bias = prm[0] + prm[1] * len_ + prm[2] * vel
|
bias = prm[0] + prm[1] * len_ + prm[2] * vel
|
||||||
|
elif typ == BiasType.MUSCLE:
|
||||||
|
bias = support.muscle_bias(len_, len_range, acc0, prm)
|
||||||
|
|
||||||
return gain * ctrl_act + bias
|
return gain * ctrl_act + bias
|
||||||
|
|
||||||
force = scan.flat(
|
force = scan.flat(
|
||||||
m,
|
m,
|
||||||
get_force,
|
get_force,
|
||||||
'uuuuuuu',
|
'uuuuuuuuu',
|
||||||
'u',
|
'u',
|
||||||
m.actuator_gaintype,
|
m.actuator_gaintype,
|
||||||
m.actuator_gainprm,
|
m.actuator_gainprm,
|
||||||
@@ -168,6 +174,8 @@ def fwd_actuation(m: Model, d: Data) -> Data:
|
|||||||
d.actuator_length,
|
d.actuator_length,
|
||||||
d.actuator_velocity,
|
d.actuator_velocity,
|
||||||
ctrl_act,
|
ctrl_act,
|
||||||
|
jp.array(m.actuator_lengthrange),
|
||||||
|
jp.array(m.actuator_acc0),
|
||||||
group_by='u',
|
group_by='u',
|
||||||
)
|
)
|
||||||
forcerange = jp.where(
|
forcerange = jp.where(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"""Tests for forward functions."""
|
"""Tests for forward functions."""
|
||||||
|
|
||||||
from absl.testing import absltest
|
from absl.testing import absltest
|
||||||
|
from absl.testing import parameterized
|
||||||
import jax
|
import jax
|
||||||
import mujoco
|
import mujoco
|
||||||
from mujoco import mjx
|
from mujoco import mjx
|
||||||
@@ -167,40 +168,28 @@ class ForwardTest(absltest.TestCase):
|
|||||||
np.testing.assert_allclose(dx.qvel, 1 + m.opt.timestep)
|
np.testing.assert_allclose(dx.qvel, 1 + m.opt.timestep)
|
||||||
|
|
||||||
|
|
||||||
class ActuatorTest(absltest.TestCase):
|
class ActuatorTest(parameterized.TestCase):
|
||||||
_DYN_XML = """
|
|
||||||
<mujoco>
|
|
||||||
<compiler autolimits="true"/>
|
|
||||||
<worldbody>
|
|
||||||
<body name="box">
|
|
||||||
<joint name="slide1" type="slide" axis="1 0 0" />
|
|
||||||
<joint name="slide2" type="slide" axis="0 1 0" />
|
|
||||||
<joint name="slide3" type="slide" axis="0 0 1" />
|
|
||||||
<joint name="slide4" type="slide" axis="1 1 0" />
|
|
||||||
<geom type="box" size=".05 .05 .05" mass="1"/>
|
|
||||||
</body>
|
|
||||||
</worldbody>
|
|
||||||
<actuator>
|
|
||||||
<general joint="slide1" dynprm="0.1" gainprm="1.1" />
|
|
||||||
<general joint="slide2" dyntype="integrator" dynprm="0.1" gainprm="1.1" />
|
|
||||||
<general joint="slide3" dyntype="filter" dynprm="0.1" gainprm="1.1" />
|
|
||||||
<general joint="slide4" dyntype="filterexact" dynprm="0.1" gainprm="1.1" />
|
|
||||||
</actuator>
|
|
||||||
</mujoco>
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_dyntype(self):
|
@parameterized.parameters(
|
||||||
m = mujoco.MjModel.from_xml_string(self._DYN_XML)
|
'actuator/arm21.xml',
|
||||||
|
'actuator/arm26.xml',
|
||||||
|
'actuator/general_dyntype.xml',
|
||||||
|
)
|
||||||
|
def test_actuator(self, fname):
|
||||||
|
m = test_util.load_test_file(fname)
|
||||||
d = mujoco.MjData(m)
|
d = mujoco.MjData(m)
|
||||||
d.ctrl = np.array([1.5, 1.5, 1.5, 1.5])
|
mujoco.mj_step(m, d)
|
||||||
d.act = np.array([0.5, 0.5, 0.5])
|
d.ctrl = 1.5 * np.random.random(m.nu)
|
||||||
|
d.act = 0.5 * np.random.random(m.na)
|
||||||
mx = mjx.put_model(m)
|
mx = mjx.put_model(m)
|
||||||
dx = mjx.put_data(m, d)
|
dx = mjx.put_data(m, d)
|
||||||
|
|
||||||
mujoco.mj_fwdActuation(m, d)
|
mujoco.mj_fwdActuation(m, d)
|
||||||
dx = jax.jit(mjx.fwd_actuation)(mx, dx)
|
dx = jax.jit(mjx.fwd_actuation)(mx, dx)
|
||||||
|
|
||||||
_assert_attr_eq(d, dx, 'act_dot')
|
_assert_attr_eq(d, dx, 'act_dot')
|
||||||
|
_assert_attr_eq(d, dx, 'qfrc_actuator')
|
||||||
|
_assert_attr_eq(d, dx, 'actuator_force')
|
||||||
|
|
||||||
mujoco.mj_Euler(m, d)
|
mujoco.mj_Euler(m, d)
|
||||||
dx = jax.jit(mjx.euler)(mx, dx)
|
dx = jax.jit(mjx.euler)(mx, dx)
|
||||||
|
|||||||
@@ -560,3 +560,150 @@ def wrap(
|
|||||||
wpnt1 = jp.where(invalid, jp.zeros(3), wpnt1)
|
wpnt1 = jp.where(invalid, jp.zeros(3), wpnt1)
|
||||||
|
|
||||||
return wlen, wpnt0, wpnt1
|
return wlen, wpnt0, wpnt1
|
||||||
|
|
||||||
|
|
||||||
|
def muscle_gain_length(
|
||||||
|
length: jax.Array, lmin: jax.Array, lmax: jax.Array
|
||||||
|
) -> jax.Array:
|
||||||
|
"""Normalized muscle length-gain curve."""
|
||||||
|
# mid-ranges (maximum is at 1.0)
|
||||||
|
a = 0.5 * (lmin + 1)
|
||||||
|
b = 0.5 * (1 + lmax)
|
||||||
|
|
||||||
|
out0 = 0.5 * jp.square(
|
||||||
|
(length - lmin) / jp.maximum(mujoco.mjMINVAL, a - lmin)
|
||||||
|
)
|
||||||
|
out1 = 1 - 0.5 * jp.square((1 - length) / jp.maximum(mujoco.mjMINVAL, 1 - a))
|
||||||
|
out2 = 1 - 0.5 * jp.square((length - 1) / jp.maximum(mujoco.mjMINVAL, b - 1))
|
||||||
|
out3 = 0.5 * jp.square(
|
||||||
|
(lmax - length) / jp.maximum(mujoco.mjMINVAL, lmax - b)
|
||||||
|
)
|
||||||
|
|
||||||
|
out = jp.where(length <= b, out2, out3)
|
||||||
|
out = jp.where(length <= 1, out1, out)
|
||||||
|
out = jp.where(length <= a, out0, out)
|
||||||
|
out = jp.where((lmin <= length) & (length <= lmax), out, 0.0)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def muscle_gain(
|
||||||
|
length: jax.Array,
|
||||||
|
vel: jax.Array,
|
||||||
|
lengthrange: jax.Array,
|
||||||
|
acc0: jax.Array,
|
||||||
|
prm: jax.Array,
|
||||||
|
) -> jax.Array:
|
||||||
|
"""Muscle active force."""
|
||||||
|
# unpack parameters
|
||||||
|
lrange = prm[:2]
|
||||||
|
force, scale, lmin, lmax, vmax, _, fvmax = prm[2:9]
|
||||||
|
|
||||||
|
force = jp.where(force < 0, scale / jp.maximum(mujoco.mjMINVAL, acc0), force)
|
||||||
|
|
||||||
|
# optimum length
|
||||||
|
L0 = (lengthrange[1] - lengthrange[0]) / jp.maximum( # pylint:disable=invalid-name
|
||||||
|
mujoco.mjMINVAL, lrange[1] - lrange[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# normalized length and velocity
|
||||||
|
L = lrange[0] + (length - lengthrange[0]) / jp.maximum(mujoco.mjMINVAL, L0) # pylint:disable=invalid-name
|
||||||
|
V = vel / jp.maximum(mujoco.mjMINVAL, L0 * vmax) # pylint:disable=invalid-name
|
||||||
|
|
||||||
|
# length curve
|
||||||
|
FL = muscle_gain_length(L, lmin, lmax) # pylint:disable=invalid-name
|
||||||
|
|
||||||
|
# velocity curve
|
||||||
|
y = fvmax - 1
|
||||||
|
FV = fvmax # pylint:disable=invalid-name
|
||||||
|
FV = jp.where( # pylint:disable=invalid-name
|
||||||
|
V <= y, fvmax - jp.square(y - V) / jp.maximum(mujoco.mjMINVAL, y), FV
|
||||||
|
)
|
||||||
|
FV = jp.where(V <= 0, jp.square(V + 1), FV) # pylint:disable=invalid-name
|
||||||
|
FV = jp.where(V <= -1, 0, FV) # pylint:disable=invalid-name
|
||||||
|
|
||||||
|
# compute FVL and scale, make it negative
|
||||||
|
return -force * FL * FV
|
||||||
|
|
||||||
|
|
||||||
|
def muscle_bias(
|
||||||
|
length: jax.Array, lengthrange: jax.Array, acc0: jax.Array, prm: jax.Array
|
||||||
|
) -> jax.Array:
|
||||||
|
"""Muscle passive force."""
|
||||||
|
# unpack parameters
|
||||||
|
lrange = prm[:2]
|
||||||
|
force, scale, _, lmax, _, fpmax = prm[2:8]
|
||||||
|
|
||||||
|
force = jp.where(force < 0, scale / jp.maximum(mujoco.mjMINVAL, acc0), force)
|
||||||
|
|
||||||
|
# optimum length
|
||||||
|
L0 = (lengthrange[1] - lengthrange[0]) / jp.maximum( # pylint:disable=invalid-name
|
||||||
|
mujoco.mjMINVAL, lrange[1] - lrange[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# normalized length
|
||||||
|
L = lrange[0] + (length - lengthrange[0]) / jp.maximum(mujoco.mjMINVAL, L0) # pylint:disable=invalid-name
|
||||||
|
|
||||||
|
# half-quadratic to (L0 + lmax) / 2, linear beyond
|
||||||
|
b = 0.5 * (1 + lmax)
|
||||||
|
|
||||||
|
out1 = (
|
||||||
|
-force
|
||||||
|
* fpmax
|
||||||
|
* 0.5
|
||||||
|
* jp.square((L - 1) / jp.maximum(mujoco.mjMINVAL, b - 1))
|
||||||
|
)
|
||||||
|
out2 = -force * fpmax * (0.5 + (L - b) / jp.maximum(mujoco.mjMINVAL, b - 1))
|
||||||
|
|
||||||
|
out = jp.where(L <= b, out1, out2)
|
||||||
|
out = jp.where(L <= 1, 0.0, out)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def muscle_dynamics_timescale(
|
||||||
|
dctrl: jax.Array,
|
||||||
|
tau_act: jax.Array,
|
||||||
|
tau_deact: jax.Array,
|
||||||
|
smoothing_width: jax.Array,
|
||||||
|
) -> jax.Array:
|
||||||
|
"""Muscle time constant with optional smoothing."""
|
||||||
|
# hard switching
|
||||||
|
tau_hard = jp.where(dctrl > 0, tau_act, tau_deact)
|
||||||
|
|
||||||
|
def _sigmoid(x):
|
||||||
|
# sigmoid function over 0 <= x <= 1 using quintic polynomial
|
||||||
|
# sigmoid: f(x) = 6 * x^5 - 15 * x^4 + 10 * x^3
|
||||||
|
# solution of f(0) = f'(0) = f''(0) = 0, f(1) = 1, f'(1) = f''(1) = 0
|
||||||
|
return jp.clip(x**3 * (3 * x * (2 * x - 5) + 10), 0, 1)
|
||||||
|
|
||||||
|
# smooth switching
|
||||||
|
# scale by width, center around 0.5 midpoint, rescale to bounds
|
||||||
|
tau_smooth = tau_deact + (tau_act - tau_deact) * _sigmoid(
|
||||||
|
dctrl / smoothing_width + 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
return jp.where(smoothing_width < mujoco.mjMINVAL, tau_hard, tau_smooth)
|
||||||
|
|
||||||
|
|
||||||
|
def muscle_dynamics(
|
||||||
|
ctrl: jax.Array, act: jax.Array, prm: jax.Array
|
||||||
|
) -> jax.Array:
|
||||||
|
"""Muscle activation dynamics."""
|
||||||
|
# clamp control
|
||||||
|
ctrlclamp = jp.clip(ctrl, 0, 1)
|
||||||
|
|
||||||
|
# clamp activation
|
||||||
|
actclamp = jp.clip(act, 0, 1)
|
||||||
|
|
||||||
|
# compute timescales as in Millard et at. (2013)
|
||||||
|
# https://doi.org/10.1115/1.4023390
|
||||||
|
tau_act = prm[0] * (0.5 + 1.5 * actclamp) # activation timescale
|
||||||
|
tau_deact = prm[1] / (0.5 + 1.5 * actclamp) # deactivation timescale
|
||||||
|
smoothing_width = prm[2] # width of smoothing sigmoid
|
||||||
|
dctrl = ctrlclamp - act # excess excitation
|
||||||
|
|
||||||
|
tau = muscle_dynamics_timescale(dctrl, tau_act, tau_deact, smoothing_width)
|
||||||
|
|
||||||
|
# filter output
|
||||||
|
return dctrl / jp.maximum(mujoco.mjMINVAL, tau)
|
||||||
|
|||||||
@@ -221,6 +221,211 @@ class SupportTest(parameterized.TestCase):
|
|||||||
force = force.at[3:].set(dx.contact.frame[j] @ force[3:])
|
force = force.at[3:].set(dx.contact.frame[j] @ force[3:])
|
||||||
np.testing.assert_allclose(result, force, rtol=1e-5, atol=2)
|
np.testing.assert_allclose(result, force, rtol=1e-5, atol=2)
|
||||||
|
|
||||||
|
def test_muscle_gain_length(self):
|
||||||
|
lmin = 0.5
|
||||||
|
lmax = 1.5
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(0, lmin, lmax),
|
||||||
|
jp.zeros(1),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(0.5, lmin, lmax),
|
||||||
|
jp.zeros(1),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(0.6, lmin, lmax),
|
||||||
|
jp.array([0.08]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(0.75, lmin, lmax),
|
||||||
|
jp.array([0.5]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(1.0, lmin, lmax),
|
||||||
|
jp.ones(1),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(1.25, lmin, lmax),
|
||||||
|
jp.array([0.5]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(1.5, lmin, lmax),
|
||||||
|
jp.zeros(1),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain_length(2.0, lmin, lmax),
|
||||||
|
jp.zeros(1),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_muscle_gain(self):
|
||||||
|
length = jp.array([1.0])
|
||||||
|
lengthrange = jp.array([0.0, 1.0])
|
||||||
|
acc0 = jp.array([1.0])
|
||||||
|
prm = jp.array([0.0, 1.0, 1.0, 200.0, 0.5, 3.0, 1.0, 0.0, 2.0, 0.0])
|
||||||
|
|
||||||
|
# V <= -1
|
||||||
|
vel = jp.array([-1.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain(length, vel, lengthrange, acc0, prm),
|
||||||
|
jp.array([-0.0]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# V <= 0
|
||||||
|
vel = jp.array([-0.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain(length, vel, lengthrange, acc0, prm),
|
||||||
|
jp.array([-0.25]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# V <= y
|
||||||
|
vel = jp.array([0.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain(length, vel, lengthrange, acc0, prm),
|
||||||
|
jp.array([-1.75]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# V > y
|
||||||
|
vel = jp.array([1.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain(length, vel, lengthrange, acc0, prm),
|
||||||
|
jp.array([-2.0]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# force < 0
|
||||||
|
prm = prm.at[2].set(-1.0)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_gain(length, vel, lengthrange, acc0, prm),
|
||||||
|
jp.array([-400.0]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_muscle_bias(self):
|
||||||
|
lengthrange = jp.array([0.0, 1.0])
|
||||||
|
acc0 = jp.array([1.0])
|
||||||
|
prm = jp.array([0.0, 1.0, 1.0, 200.0, 0.5, 3.0, 1.5, 1.3, 1.2, 0.0])
|
||||||
|
|
||||||
|
# L <= 1
|
||||||
|
length = jp.array([0.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_bias(length, lengthrange, acc0, prm),
|
||||||
|
jp.array([0.0]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# L <= b
|
||||||
|
length = jp.array([1.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_bias(length, lengthrange, acc0, prm),
|
||||||
|
jp.array([-0.1625]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# L > b
|
||||||
|
length = jp.array([2.5])
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_bias(length, lengthrange, acc0, prm),
|
||||||
|
jp.array([-1.3]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# force < 0
|
||||||
|
prm = prm.at[2].set(-1.0)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
support.muscle_bias(length, lengthrange, acc0, prm),
|
||||||
|
jp.array([-260.0]),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_smooth_muscle_dynamics(self):
|
||||||
|
# compute time constant as in Millard et al. (2013)
|
||||||
|
# https://doi.org/10.1115/1.4023390
|
||||||
|
def _muscle_dynamics_millard(ctrl, act, prm):
|
||||||
|
ctrlclamp = jp.clip(ctrl, 0, 1)
|
||||||
|
actclamp = jp.clip(act, 0, 1)
|
||||||
|
|
||||||
|
tau0 = prm[0] * (0.5 + 1.5 * actclamp)
|
||||||
|
tau1 = prm[1] / (0.5 + 1.5 * actclamp)
|
||||||
|
tau = jp.where(ctrlclamp > act, tau0, tau1)
|
||||||
|
|
||||||
|
return (ctrlclamp - act) / jp.maximum(mujoco.mjMINVAL, tau)
|
||||||
|
|
||||||
|
prm = jp.array([0.01, 0.04, 0.0])
|
||||||
|
|
||||||
|
# exact equality if tau_smooth = 0
|
||||||
|
for ctrl in [-0.1, 0.0, 0.4, 0.5, 1.0, 1.0]:
|
||||||
|
for act in [-0.1, 0.0, 0.4, 0.5, 1.0, 1.1]:
|
||||||
|
actdot_old = _muscle_dynamics_millard(ctrl, act, prm)
|
||||||
|
actdot_new = support.muscle_dynamics(ctrl, act, prm)
|
||||||
|
np.testing.assert_allclose(actdot_old, actdot_new, rtol=1e-5, atol=1e-5)
|
||||||
|
|
||||||
|
# positive tau_smooth
|
||||||
|
tau_smooth = 0.2
|
||||||
|
prm = prm.at[2].set(tau_smooth)
|
||||||
|
act = 0.5
|
||||||
|
eps = 1.0e-6
|
||||||
|
|
||||||
|
ctrl = 0.4 - eps # smaller than act by just over 0.5 * tau_smooth
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
_muscle_dynamics_millard(ctrl, act, prm),
|
||||||
|
support.muscle_dynamics(ctrl, act, prm),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctrl = 0.6 + eps # larger than act by just over 0.5 * tau_smooth
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
_muscle_dynamics_millard(ctrl, act, prm),
|
||||||
|
support.muscle_dynamics(ctrl, act, prm),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
# right in the middle should give average of time constants
|
||||||
|
tau_act = 0.2
|
||||||
|
tau_deact = 0.3
|
||||||
|
for dctrl in [0.0, 0.1, 0.2, 1.0, 1.1]:
|
||||||
|
lower = support.muscle_dynamics_timescale(
|
||||||
|
-dctrl, tau_act, tau_deact, tau_smooth
|
||||||
|
)
|
||||||
|
upper = support.muscle_dynamics_timescale(
|
||||||
|
dctrl, tau_act, tau_deact, tau_smooth
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
0.5 * (upper + lower),
|
||||||
|
0.5 * (tau_act + tau_deact),
|
||||||
|
rtol=1e-5,
|
||||||
|
atol=1e-5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
absltest.main()
|
absltest.main()
|
||||||
|
|||||||
@@ -234,12 +234,14 @@ class DynType(enum.IntEnum):
|
|||||||
INTEGRATOR: integrator: da/dt = u
|
INTEGRATOR: integrator: da/dt = u
|
||||||
FILTER: linear filter: da/dt = (u-a) / tau
|
FILTER: linear filter: da/dt = (u-a) / tau
|
||||||
FILTEREXACT: linear filter: da/dt = (u-a) / tau, with exact integration
|
FILTEREXACT: linear filter: da/dt = (u-a) / tau, with exact integration
|
||||||
|
MUSCLE: piece-wise linear filter with two time constants
|
||||||
"""
|
"""
|
||||||
NONE = mujoco.mjtDyn.mjDYN_NONE
|
NONE = mujoco.mjtDyn.mjDYN_NONE
|
||||||
INTEGRATOR = mujoco.mjtDyn.mjDYN_INTEGRATOR
|
INTEGRATOR = mujoco.mjtDyn.mjDYN_INTEGRATOR
|
||||||
FILTER = mujoco.mjtDyn.mjDYN_FILTER
|
FILTER = mujoco.mjtDyn.mjDYN_FILTER
|
||||||
FILTEREXACT = mujoco.mjtDyn.mjDYN_FILTEREXACT
|
FILTEREXACT = mujoco.mjtDyn.mjDYN_FILTEREXACT
|
||||||
# unsupported: MUSCLE, USER
|
MUSCLE = mujoco.mjtDyn.mjDYN_MUSCLE
|
||||||
|
# unsupported: USER
|
||||||
|
|
||||||
|
|
||||||
class GainType(enum.IntEnum):
|
class GainType(enum.IntEnum):
|
||||||
@@ -248,10 +250,12 @@ class GainType(enum.IntEnum):
|
|||||||
Members:
|
Members:
|
||||||
FIXED: fixed gain
|
FIXED: fixed gain
|
||||||
AFFINE: const + kp*length + kv*velocity
|
AFFINE: const + kp*length + kv*velocity
|
||||||
|
MUSCLE: muscle FLV curve computed by muscle_gain
|
||||||
"""
|
"""
|
||||||
FIXED = mujoco.mjtGain.mjGAIN_FIXED
|
FIXED = mujoco.mjtGain.mjGAIN_FIXED
|
||||||
AFFINE = mujoco.mjtGain.mjGAIN_AFFINE
|
AFFINE = mujoco.mjtGain.mjGAIN_AFFINE
|
||||||
# unsupported: MUSCLE, USER
|
MUSCLE = mujoco.mjtGain.mjGAIN_MUSCLE
|
||||||
|
# unsupported: USER
|
||||||
|
|
||||||
|
|
||||||
class BiasType(enum.IntEnum):
|
class BiasType(enum.IntEnum):
|
||||||
@@ -260,10 +264,12 @@ class BiasType(enum.IntEnum):
|
|||||||
Members:
|
Members:
|
||||||
NONE: no bias
|
NONE: no bias
|
||||||
AFFINE: const + kp*length + kv*velocity
|
AFFINE: const + kp*length + kv*velocity
|
||||||
|
MUSCLE: muscle passive force computed by muscle_bias
|
||||||
"""
|
"""
|
||||||
NONE = mujoco.mjtBias.mjBIAS_NONE
|
NONE = mujoco.mjtBias.mjBIAS_NONE
|
||||||
AFFINE = mujoco.mjtBias.mjBIAS_AFFINE
|
AFFINE = mujoco.mjtBias.mjBIAS_AFFINE
|
||||||
# unsupported: MUSCLE, USER
|
MUSCLE = mujoco.mjtBias.mjBIAS_MUSCLE
|
||||||
|
# unsupported: USER
|
||||||
|
|
||||||
|
|
||||||
class ConstraintType(enum.IntEnum):
|
class ConstraintType(enum.IntEnum):
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<!-- Copyright 2021 DeepMind Technologies Limited
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<mujoco model="2-link 1-muscle arm">
|
||||||
|
<worldbody>
|
||||||
|
<body>
|
||||||
|
<geom type="capsule" size="0.01" fromto="0 0 0 1 0 0"/>
|
||||||
|
<site name="s0" pos="0 0 0.01"/>
|
||||||
|
<joint type="slide" axis="1 0 0"/>
|
||||||
|
<body pos="1 0 0">
|
||||||
|
<geom type="capsule" size="0.01" fromto="0 0 0 1 0 0"/>
|
||||||
|
<site name="s1" pos="1 0 0.01"/>
|
||||||
|
</body>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
<tendon>
|
||||||
|
<spatial name="tendon" width="0.01">
|
||||||
|
<site site="s0"/>
|
||||||
|
<site site="s1"/>
|
||||||
|
</spatial>
|
||||||
|
</tendon>
|
||||||
|
<actuator>
|
||||||
|
<muscle tendon="tendon" lengthrange="0 2" force="1" ctrllimited="true" ctrlrange="0 1"/>
|
||||||
|
</actuator>
|
||||||
|
</mujoco>
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<!-- Copyright 2021 DeepMind Technologies Limited
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<mujoco model="2-link 6-muscle arm">
|
||||||
|
<option timestep="0.005" iterations="50" solver="Newton" tolerance="1e-10"/>
|
||||||
|
|
||||||
|
<visual>
|
||||||
|
<rgba haze=".3 .3 .3 1"/>
|
||||||
|
</visual>
|
||||||
|
|
||||||
|
<default>
|
||||||
|
<joint type="hinge" pos="0 0 0" axis="0 0 1" limited="true" range="0 120" damping="0.1"/>
|
||||||
|
<muscle ctrllimited="true" ctrlrange="0 1"/>
|
||||||
|
</default>
|
||||||
|
|
||||||
|
<asset>
|
||||||
|
<texture type="skybox" builtin="gradient" rgb1="0.6 0.6 0.6" rgb2="0 0 0" width="512" height="512"/>
|
||||||
|
|
||||||
|
<texture name="texplane" type="2d" builtin="checker" rgb1=".25 .25 .25" rgb2=".3 .3 .3" width="512" height="512" mark="cross" markrgb=".8 .8 .8"/>
|
||||||
|
|
||||||
|
<material name="matplane" reflectance="0.3" texture="texplane" texrepeat="1 1" texuniform="true"/>
|
||||||
|
</asset>
|
||||||
|
|
||||||
|
<worldbody>
|
||||||
|
<geom name="floor" pos="0 0 -0.5" size="0 0 1" type="plane" material="matplane"/>
|
||||||
|
|
||||||
|
<light directional="true" diffuse=".8 .8 .8" specular=".2 .2 .2" pos="0 0 5" dir="0 0 -1"/>
|
||||||
|
|
||||||
|
<site name="s0" pos="-0.15 0 0" size="0.02"/>
|
||||||
|
<site name="x0" pos="0 -0.15 0" size="0.02" rgba="0 .7 0 1" group="1"/>
|
||||||
|
|
||||||
|
<body pos="0 0 0">
|
||||||
|
<geom name="upper arm" type="capsule" size="0.045" fromto="0 0 0 0.5 0 0" rgba=".5 .1 .1 1"/>
|
||||||
|
<joint name="shoulder"/>
|
||||||
|
<geom name="shoulder" type="cylinder" pos="0 0 0" size=".1 .05" rgba=".5 .1 .8 .5" mass="0" group="1"/>
|
||||||
|
|
||||||
|
<site name="s1" pos="0.15 0.06 0" size="0.02"/>
|
||||||
|
<site name="s2" pos="0.15 -0.06 0" size="0.02"/>
|
||||||
|
<site name="s3" pos="0.4 0.06 0" size="0.02"/>
|
||||||
|
<site name="s4" pos="0.4 -0.06 0" size="0.02"/>
|
||||||
|
<site name="s5" pos="0.25 0.1 0" size="0.02"/>
|
||||||
|
<site name="s6" pos="0.25 -0.1 0" size="0.02"/>
|
||||||
|
<site name="x1" pos="0.5 -0.15 0" size="0.02" rgba="0 .7 0 1" group="1"/>
|
||||||
|
|
||||||
|
<body pos="0.5 0 0">
|
||||||
|
<geom name="forearm" type="capsule" size="0.035" fromto="0 0 0 0.5 0 0" rgba=".5 .1 .1 1"/>
|
||||||
|
<joint name="elbow"/>
|
||||||
|
<geom name="elbow" type="cylinder" pos="0 0 0" size=".08 .05" rgba=".5 .1 .8 .5" mass="0" group="1"/>
|
||||||
|
|
||||||
|
<site name="s7" pos="0.11 0.05 0" size="0.02"/>
|
||||||
|
<site name="s8" pos="0.11 -0.05 0" size="0.02"/>
|
||||||
|
</body>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
|
||||||
|
<tendon>
|
||||||
|
<spatial name="SF" width="0.01">
|
||||||
|
<site site="s0"/>
|
||||||
|
<geom geom="shoulder"/>
|
||||||
|
<site site="s1"/>
|
||||||
|
</spatial>
|
||||||
|
|
||||||
|
<spatial name="SE" width="0.01">
|
||||||
|
<site site="s0"/>
|
||||||
|
<geom geom="shoulder" sidesite="x0"/>
|
||||||
|
<site site="s2"/>
|
||||||
|
</spatial>
|
||||||
|
|
||||||
|
<spatial name="EF" width="0.01">
|
||||||
|
<site site="s3"/>
|
||||||
|
<geom geom="elbow"/>
|
||||||
|
<site site="s7"/>
|
||||||
|
</spatial>
|
||||||
|
|
||||||
|
<spatial name="EE" width="0.01">
|
||||||
|
<site site="s4"/>
|
||||||
|
<geom geom="elbow" sidesite="x1"/>
|
||||||
|
<site site="s8"/>
|
||||||
|
</spatial>
|
||||||
|
|
||||||
|
<spatial name="BF" width="0.009" rgba=".4 .6 .4 1">
|
||||||
|
<site site="s0"/>
|
||||||
|
<geom geom="shoulder"/>
|
||||||
|
<site site="s5"/>
|
||||||
|
<geom geom="elbow"/>
|
||||||
|
<site site="s7"/>
|
||||||
|
</spatial>
|
||||||
|
|
||||||
|
<spatial name="BE" width="0.009" rgba=".4 .6 .4 1">
|
||||||
|
<site site="s0"/>
|
||||||
|
<geom geom="shoulder" sidesite="x0"/>
|
||||||
|
<site site="s6"/>
|
||||||
|
<geom geom="elbow" sidesite="x1"/>
|
||||||
|
<site site="s8"/>
|
||||||
|
</spatial>
|
||||||
|
</tendon>
|
||||||
|
|
||||||
|
<actuator>
|
||||||
|
<muscle name="SF" tendon="SF"/>
|
||||||
|
<muscle name="SE" tendon="SE"/>
|
||||||
|
<muscle name="EF" tendon="EF"/>
|
||||||
|
<muscle name="EE" tendon="EE"/>
|
||||||
|
<muscle name="BF" tendon="BF"/>
|
||||||
|
<muscle name="BE" tendon="BE"/>
|
||||||
|
</actuator>
|
||||||
|
</mujoco>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<mujoco>
|
||||||
|
<compiler autolimits="true"/>
|
||||||
|
<worldbody>
|
||||||
|
<body name="box">
|
||||||
|
<joint name="slide1" type="slide" axis="1 0 0" />
|
||||||
|
<joint name="slide2" type="slide" axis="0 1 0" />
|
||||||
|
<joint name="slide3" type="slide" axis="0 0 1" />
|
||||||
|
<joint name="slide4" type="slide" axis="1 1 0" />
|
||||||
|
<geom type="box" size=".05 .05 .05" mass="1"/>
|
||||||
|
</body>
|
||||||
|
</worldbody>
|
||||||
|
<actuator>
|
||||||
|
<general joint="slide1" dynprm="0.1" gainprm="1.1" />
|
||||||
|
<general joint="slide2" dyntype="integrator" dynprm="0.1" gainprm="1.1" />
|
||||||
|
<general joint="slide3" dyntype="filter" dynprm="0.1" gainprm="1.1" />
|
||||||
|
<general joint="slide4" dyntype="filterexact" dynprm="0.1" gainprm="1.1" />
|
||||||
|
</actuator>
|
||||||
|
</mujoco>
|
||||||
Reference in New Issue
Block a user