Switch MJX DataJAX from legacy qM format to CSR M format

Switch MJX DataJAX to use the CSR-format `M` as its primary sparse inertia representation, matching MuJoCo C `mjData.M`. This allows both `mjData.qM` and `mjModel.mapM2M` to be deleted in the future.

PiperOrigin-RevId: 932433465
Change-Id: I194ffbfdba0cdd9c718277ee0d5de7ac20f2b06f
This commit is contained in:
Yuval Tassa
2026-06-15 06:52:12 -07:00
committed by Copybara-Service
parent 3317921999
commit 60b5cac066
7 changed files with 96 additions and 123 deletions
+5 -4
View File
@@ -352,10 +352,11 @@ def euler(m: Model, d: Data) -> Data:
qacc = d.qacc qacc = d.qacc
if not m.opt.disableflags & DisableBit.EULERDAMP: if not m.opt.disableflags & DisableBit.EULERDAMP:
if support.is_sparse(m): if support.is_sparse(m):
qM = d._impl.qM.at[m.dof_Madr].add(m.opt.timestep * m.dof_damping) diag_adr = m.M_rowadr + m.M_rownnz - 1
M = d._impl.M.at[diag_adr].add(m.opt.timestep * m.dof_damping)
else: else:
qM = d._impl.qM + jp.diag(m.opt.timestep * m.dof_damping) M = d._impl.M + jp.diag(m.opt.timestep * m.dof_damping)
dh = d.tree_replace({'_impl.qM': qM}) dh = d.tree_replace({'_impl.M': M})
dh = smooth.factor_m(m, dh) dh = smooth.factor_m(m, dh)
qfrc = d.qfrc_smooth + d.qfrc_constraint qfrc = d.qfrc_smooth + d.qfrc_constraint
qacc = smooth.solve_m(m, dh, qfrc) qacc = smooth.solve_m(m, dh, qfrc)
@@ -418,7 +419,7 @@ def implicit(m: Model, d: Data) -> Data:
qacc = d.qacc qacc = d.qacc
if qderiv is not None: if qderiv is not None:
# TODO(robotics-simulation): use smooth.factor_m / solve_m here: # TODO(robotics-simulation): use smooth.factor_m / solve_m here:
qm = support.full_m(m, d) if support.is_sparse(m) else d._impl.qM qm = support.full_m(m, d) if support.is_sparse(m) else d._impl.M
qm -= m.opt.timestep * qderiv qm -= m.opt.timestep * qderiv
qh, _ = jax.scipy.linalg.cho_factor(qm) qh, _ = jax.scipy.linalg.cho_factor(qm)
qfrc = d.qfrc_smooth + d.qfrc_constraint qfrc = d.qfrc_smooth + d.qfrc_constraint
+17 -21
View File
@@ -669,8 +669,7 @@ def _make_data_jax(
'wrap_xpos': (m.nwrap, 6, float_), 'wrap_xpos': (m.nwrap, 6, float_),
'actuator_moment': (m.nu, m.nv, float_), 'actuator_moment': (m.nu, m.nv, float_),
'crb': (m.nbody, 10, float_), 'crb': (m.nbody, 10, float_),
'qM': (m.nM, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), 'M': (m.nC, float_) if support.is_sparse(m) else (m.nv, m.nv, float_),
'M': (m.nC, float_),
'qLD': (m.nC, float_) if support.is_sparse(m) else (m.nv, m.nv, float_), 'qLD': (m.nC, float_) if support.is_sparse(m) else (m.nv, m.nv, float_),
'qLDiagInv': (m.nv, float_) if support.is_sparse(m) else (0, float_), 'qLDiagInv': (m.nv, float_) if support.is_sparse(m) else (0, float_),
'ten_velocity': (m.ntendon, float_), 'ten_velocity': (m.ntendon, float_),
@@ -1045,11 +1044,11 @@ def _put_data_jax(
impl_fields[fname] = value impl_fields[fname] = value
# convert qM and qLD if jacobian is dense # convert M and qLD if jacobian is dense
if not support.is_sparse(m): if not support.is_sparse(m):
impl_fields['qM'] = np.zeros((m.nv, m.nv)) impl_fields['M'] = np.zeros((m.nv, m.nv))
mujoco.mju_sym2dense( mujoco.mju_sym2dense(
impl_fields['qM'], impl_fields['M'],
d.M, d.M,
m.M_rownnz, m.M_rownnz,
m.M_rowadr, m.M_rowadr,
@@ -1057,9 +1056,9 @@ def _put_data_jax(
) )
# TODO(erikfrey): derive L*L' from L'*D*L instead of recomputing # TODO(erikfrey): derive L*L' from L'*D*L instead of recomputing
try: try:
impl_fields['qLD'], _ = scipy.linalg.cho_factor(impl_fields['qM']) impl_fields['qLD'], _ = scipy.linalg.cho_factor(impl_fields['M'])
except scipy.linalg.LinAlgError: except scipy.linalg.LinAlgError:
# this happens when qM is empty or unstable simulation # this happens when M is empty or unstable simulation
impl_fields['qLD'] = np.zeros((m.nv, m.nv)) impl_fields['qLD'] = np.zeros((m.nv, m.nv))
impl_fields['qLDiagInv'] = np.zeros(0) impl_fields['qLDiagInv'] = np.zeros(0)
@@ -1333,15 +1332,6 @@ def _get_data_into(
d = jax.device_get(d) d = jax.device_get(d)
batch_size = d.qpos.shape[0] if batched else 1 batch_size = d.qpos.shape[0] if batched else 1
dof_i, dof_j = [], []
if d.impl == types.Impl.JAX:
for i in range(m.nv):
j = i
while j > -1:
dof_i.append(i)
dof_j.append(j)
j = m.dof_parentid[j]
for i in range(batch_size): for i in range(batch_size):
d_i = jax.tree_util.tree_map(lambda x, i=i: x[i], d) if batched else d d_i = jax.tree_util.tree_map(lambda x, i=i: x[i], d) if batched else d
result_i = result[i] if batched else result result_i = result[i] if batched else result
@@ -1446,8 +1436,14 @@ def _get_data_into(
elif field.name.startswith('efc_'): elif field.name.startswith('efc_'):
value = value[efc_active] value = value[efc_active]
if d.impl == types.Impl.JAX: if d.impl == types.Impl.JAX:
if field.name == 'qM' and not support.is_sparse(m): if field.name == 'M' and not support.is_sparse(m):
value = value[dof_i, dof_j] M_csr = np.zeros(m.nC)
for i in range(m.nv):
adr = m.M_rowadr[i]
for k in range(m.M_rownnz[i]):
col = m.M_colind[adr + k]
M_csr[adr + k] = value[i, col]
value = M_csr
elif field.name == 'qLD': elif field.name == 'qLD':
value = np.zeros(m.nC) value = np.zeros(m.nC)
elif field.name == 'qLDiagInv' and not support.is_sparse(m): elif field.name == 'qLDiagInv' and not support.is_sparse(m):
@@ -1464,9 +1460,9 @@ def _get_data_into(
else: else:
setattr(result_i, field.name, value) setattr(result_i, field.name, value)
# TODO(taylorhowell): remove mapping once qM is deprecated if hasattr(result_i, 'qM'):
# map inertia (sparse) to reduced inertia (compressed sparse) representation result_i.qM.fill(0.0)
result_i.M[:] = result_i.qM[m.mapM2M] result_i.qM[m.mapM2M] = result_i.M
# recalculate qLD and qLDiagInv as MJX and MuJoCo have different # recalculate qLD and qLDiagInv as MJX and MuJoCo have different
# representations of the Cholesky decomposition. # representations of the Cholesky decomposition.
+7 -9
View File
@@ -476,20 +476,18 @@ class DataIOTest(parameterized.TestCase):
self.assertEqual(d._impl.efc_force.shape, (nefc,)) self.assertEqual(d._impl.efc_force.shape, (nefc,))
if impl == 'jax': if impl == 'jax':
self.assertEqual(d._impl.qM.shape, (nv, nv)) self.assertEqual(d._impl.M.shape, (nv, nv))
self.assertEqual(d._impl.qLD.shape, (nv, nv)) self.assertEqual(d._impl.qLD.shape, (nv, nv))
self.assertEqual(d._impl.qLDiagInv.shape, (0,)) self.assertEqual(d._impl.qLDiagInv.shape, (0,))
# test sparse # test sparse
m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE
d = mjx.make_data(m, impl=impl) d = mjx.make_data(m, impl=impl)
self.assertEqual(d._impl.qM.shape, (nm,)) self.assertEqual(d._impl.M.shape, (m.nC,))
self.assertEqual(d._impl.qLD.shape, (nm,)) self.assertEqual(d._impl.qLD.shape, (m.nC,))
self.assertEqual(d._impl.qLDiagInv.shape, (nv,)) self.assertEqual(d._impl.qLDiagInv.shape, (nv,))
@mock.patch.dict(os.environ, {'MJX_GPU_DEFAULT_WARP': 'true'}) @mock.patch.dict(os.environ, {'MJX_GPU_DEFAULT_WARP': 'true'})
def test_make_data_warp(self): def test_make_data_warp(self):
if not mjxw.WARP_INSTALLED: if not mjxw.WARP_INSTALLED:
@@ -605,7 +603,7 @@ class DataIOTest(parameterized.TestCase):
) )
# check sparse mass matrices are correct # check sparse mass matrices are correct
np.testing.assert_allclose(dx_sparse._impl.qM, d.qM, atol=1e-8) np.testing.assert_allclose(dx_sparse._impl.M, d.M, atol=1e-8)
np.testing.assert_allclose(dx_sparse._impl.qLD, d.qLD, atol=1e-8) np.testing.assert_allclose(dx_sparse._impl.qLD, d.qLD, atol=1e-8)
np.testing.assert_allclose( np.testing.assert_allclose(
dx_sparse._impl.qLDiagInv, d.qLDiagInv, atol=1e-8 dx_sparse._impl.qLDiagInv, d.qLDiagInv, atol=1e-8
@@ -619,7 +617,7 @@ class DataIOTest(parameterized.TestCase):
if impl == 'jax': if impl == 'jax':
qm = np.zeros((m.nv, m.nv)) qm = np.zeros((m.nv, m.nv))
mujoco.mju_sym2dense(qm, d.M, m.M_rownnz, m.M_rowadr, m.M_colind) mujoco.mju_sym2dense(qm, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
np.testing.assert_allclose(dx_from_dense._impl.qM, qm, atol=1e-8) np.testing.assert_allclose(dx_from_dense._impl.M, qm, atol=1e-8)
def test_put_data_warp_ndim(self): def test_put_data_warp_ndim(self):
@@ -865,8 +863,8 @@ class DataIOTest(parameterized.TestCase):
mjx.make_data(m) mjx.make_data(m)
@parameterized.parameters(JacobianType.DENSE, JacobianType.SPARSE) @parameterized.parameters(JacobianType.DENSE, JacobianType.SPARSE)
def test_qm_mapm2m(self, jacobian): def test_m_mapm2m(self, jacobian):
"""Test that qM is mapped to M.""" """Test that M matches MuJoCo."""
m = test_util.load_test_file('humanoid/humanoid.xml') m = test_util.load_test_file('humanoid/humanoid.xml')
m.opt.jacobian = jacobian m.opt.jacobian = jacobian
d = mujoco.MjData(m) d = mujoco.MjData(m)
+37 -45
View File
@@ -307,7 +307,7 @@ def crb(m: Model, d: Data) -> Data:
crb_dof = jp.take(crb_body, jp.array(m.dof_bodyid), axis=0) crb_dof = jp.take(crb_body, jp.array(m.dof_bodyid), axis=0)
crb_cdof = jax.vmap(math.inert_mul)(crb_dof, d.cdof) crb_cdof = jax.vmap(math.inert_mul)(crb_dof, d.cdof)
qm = support.make_m(m, crb_cdof, d.cdof, m.dof_armature) qm = support.make_m(m, crb_cdof, d.cdof, m.dof_armature)
d = d.tree_replace({'_impl.qM': qm}) d = d.tree_replace({'_impl.M': qm})
return d return d
@@ -317,56 +317,55 @@ def factor_m(m: Model, d: Data) -> Data:
raise ValueError('factor_m requires JAX backend implementation.') raise ValueError('factor_m requires JAX backend implementation.')
if not support.is_sparse(m): if not support.is_sparse(m):
qh, _ = jax.scipy.linalg.cho_factor(d._impl.qM) qh, _ = jax.scipy.linalg.cho_factor(d._impl.M)
d = d.tree_replace({'_impl.qLD': qh}) d = d.tree_replace({'_impl.qLD': qh})
return d return d
# build up indices for where we will do backwards updates over qLD
depth = [] depth = []
for i in range(m.nv): for i in range(m.nv):
depth.append(depth[m.dof_parentid[i]] + 1 if m.dof_parentid[i] != -1 else 0) depth.append(depth[m.dof_parentid[i]] + 1 if m.dof_parentid[i] != -1 else 0)
updates = {} updates = {}
madr_ds = [] diag_ds = []
for i in range(m.nv): for i in range(m.nv):
madr_d = madr_ij = m.dof_Madr[i] diag_i = m.M_rowadr[i] + m.M_rownnz[i] - 1
j = i start_i = m.M_rowadr[i]
while True: for k in range(m.M_rownnz[i]):
madr_ds.append(madr_d) diag_ds.append(diag_i)
madr_ij, j = madr_ij + 1, m.dof_parentid[j] for k in range(m.M_rownnz[i] - 1):
if j == -1: adr = start_i + k
break j = m.M_colind[adr]
out_beg, out_end = tuple(m.dof_Madr[j : j + 2]) out_beg = m.M_rowadr[j]
width = m.M_rownnz[j]
out_end = out_beg + width
updates.setdefault(depth[j], []).append( updates.setdefault(depth[j], []).append(
(out_beg, out_end, madr_d, madr_ij) (out_beg, out_end, diag_i, adr, start_i)
) )
qld = d._impl.qM qld = d._impl.M
for _, updates in sorted(updates.items(), reverse=True): for _, updates in sorted(updates.items(), reverse=True):
# combine the updates into one update batch (per depth level)
rows = [] rows = []
madr_ijs = [] madr_ijs = []
pivots = [] pivots = []
out = [] out = []
for b, e, madr_d, madr_ij in updates: for b, e, piv_i, madr_ij, start_i in updates:
width = e - b width = e - b
rows.append(np.arange(madr_ij, madr_ij + width)) rows.append(np.arange(start_i, start_i + width))
madr_ijs.append(np.full((width,), madr_ij)) madr_ijs.append(np.full((width,), madr_ij))
pivots.append(np.full((width,), madr_d)) pivots.append(np.full((width,), piv_i))
out.append(np.arange(b, e)) out.append(np.arange(b, e))
rows = np.concatenate(rows) if out:
madr_ijs = np.concatenate(madr_ijs) rows = np.concatenate(rows)
pivots = np.concatenate(pivots) madr_ijs = np.concatenate(madr_ijs)
out = np.concatenate(out) pivots = np.concatenate(pivots)
out = np.concatenate(out)
# apply the update batch qld = qld.at[out].add(-(qld[madr_ijs] / qld[pivots]) * qld[rows])
qld = qld.at[out].add(-(qld[madr_ijs] / qld[pivots]) * qld[rows])
# TODO(erikfrey): determine if this minimum value guarding is necessary:
# qld = qld.at[dof_madr].set(jp.maximum(qld[dof_madr], _MJ_MINVAL))
qld_diag = qld[m.dof_Madr] diag_adr = m.M_rowadr + m.M_rownnz - 1
qld = (qld / qld[jp.array(madr_ds)]).at[m.dof_Madr].set(qld_diag) qld_diag = qld[diag_adr]
qld = (qld / qld[np.array(diag_ds)]).at[diag_adr].set(qld_diag)
d = d.tree_replace({'_impl.qLD': qld, '_impl.qLDiagInv': 1 / qld_diag}) d = d.tree_replace({'_impl.qLD': qld, '_impl.qLDiagInv': 1 / qld_diag})
return d return d
@@ -386,13 +385,12 @@ def solve_m(m: Model, d: Data, x: jax.Array) -> jax.Array:
updates_i, updates_j = {}, {} updates_i, updates_j = {}, {}
for i in range(m.nv): for i in range(m.nv):
madr_ij, j = m.dof_Madr[i], i start_i = m.M_rowadr[i]
while True: for k in range(m.M_rownnz[i] - 1):
madr_ij, j = madr_ij + 1, m.dof_parentid[j] adr = start_i + k
if j == -1: j = m.M_colind[adr]
break updates_i.setdefault(depth[i], []).append((i, adr, j))
updates_i.setdefault(depth[i], []).append((i, madr_ij, j)) updates_j.setdefault(depth[j], []).append((j, adr, i))
updates_j.setdefault(depth[j], []).append((j, madr_ij, i))
# x <- inv(L') * x # x <- inv(L') * x
for _, vals in sorted(updates_j.items(), reverse=True): for _, vals in sorted(updates_j.items(), reverse=True):
@@ -1317,7 +1315,7 @@ def transmission(m: Model, d: Data) -> Data:
def tendon_armature(m: Model, d: Data) -> Data: def tendon_armature(m: Model, d: Data) -> Data:
"""Add tendon armature to qM.""" """Add tendon armature to M."""
if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX): if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX):
raise ValueError('tendon_armature requires JAX backend implementation.') raise ValueError('tendon_armature requires JAX backend implementation.')
@@ -1330,17 +1328,11 @@ def tendon_armature(m: Model, d: Data) -> Data:
) )
if support.is_sparse(m): if support.is_sparse(m):
ij = [] i = np.repeat(np.arange(m.nv), m.M_rownnz)
for i in range(m.nv): j = m.M_colind
j = i
while j > -1:
ij.append((i, j))
j = m.dof_parentid[j]
i, j = (jp.array(x) for x in zip(*ij))
JTAJ = JTAJ[(i, j)] JTAJ = JTAJ[(i, j)]
return d.tree_replace({'_impl.qM': d._impl.qM + JTAJ}) return d.tree_replace({'_impl.M': d._impl.M + JTAJ})
def tendon_dot(m: Model, d: Data) -> jax.Array: def tendon_dot(m: Model, d: Data) -> jax.Array:
+7 -10
View File
@@ -89,13 +89,10 @@ class SmoothTest(absltest.TestCase):
# crb # crb
dx = jax.jit(mjx.crb)(mx, mjx.put_data(m, d)) dx = jax.jit(mjx.crb)(mx, mjx.put_data(m, d))
_assert_attr_eq(d, dx._impl, 'crb') _assert_attr_eq(d, dx._impl, 'crb')
_assert_attr_eq(d, dx._impl, 'qM') _assert_attr_eq(d, dx._impl, 'M')
# factor_m # factor_m
dx = jax.jit(mjx.factor_m)(mx, mjx.put_data(m, d)) dx = jax.jit(mjx.factor_m)(mx, mjx.put_data(m, d))
qLDLegacy = np.zeros(mx.nM) # pylint:disable=invalid-name _assert_attr_eq(d, dx._impl, 'qLD')
for i in range(m.nC):
qLDLegacy[m.mapM2M[i]] = d.qLD[i]
_assert_eq(qLDLegacy, dx._impl.qLD, 'qLD')
_assert_attr_eq(d, dx._impl, 'qLDiagInv') _assert_attr_eq(d, dx._impl, 'qLDiagInv')
# com_vel # com_vel
dx = jax.jit(mjx.com_vel)(mx, mjx.put_data(m, d)) dx = jax.jit(mjx.com_vel)(mx, mjx.put_data(m, d))
@@ -430,18 +427,18 @@ class TendonTest(parameterized.TestCase):
dx = mjx.put_data(m, d) dx = mjx.put_data(m, d)
dx = dx.tree_replace( dx = dx.tree_replace(
{'_impl.qM': jp.zeros((m.nv, m.nv)), 'qfrc_bias': jp.zeros(m.nv)} {'_impl.M': jp.zeros((m.nv, m.nv)), 'qfrc_bias': jp.zeros(m.nv)}
) )
dx = mjx.crb(mx, dx) dx = mjx.crb(mx, dx)
dx = mjx.tendon_armature(mx, dx) dx = mjx.tendon_armature(mx, dx)
if jacobian == JacobianType.DENSE: if jacobian == JacobianType.DENSE:
qM = np.zeros((m.nv, m.nv)) # pylint: disable=invalid-name M = np.zeros((m.nv, m.nv)) # pylint: disable=invalid-name
mujoco.mju_sym2dense(qM, d.M, m.M_rownnz, m.M_rowadr, m.M_colind) mujoco.mju_sym2dense(M, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
else: else:
qM = d.qM # pylint: disable=invalid-name M = d.M # pylint: disable=invalid-name
_assert_eq(dx._impl.qM, qM, 'qM') _assert_eq(dx._impl.M, M, 'M')
dx = mjx.rne(mx, dx) dx = mjx.rne(mx, dx)
dx = mjx.tendon_bias(mx, dx) dx = mjx.tendon_bias(mx, dx)
+20 -33
View File
@@ -60,14 +60,8 @@ def make_m(
) -> jax.Array: ) -> jax.Array:
"""Computes M = a @ b.T + diag(d).""" """Computes M = a @ b.T + diag(d)."""
ij = [] i = np.repeat(np.arange(m.nv), m.M_rownnz)
for i in range(m.nv): j = m.M_colind
j = i
while j > -1:
ij.append((i, j))
j = m.dof_parentid[j]
i, j = (jp.array(x) for x in zip(*ij))
if not is_sparse(m): if not is_sparse(m):
qm = a @ b.T qm = a @ b.T
@@ -82,29 +76,23 @@ def make_m(
b_j = jp.take(b, j, axis=0) b_j = jp.take(b, j, axis=0)
qm = jax.vmap(jp.dot)(a_i, b_j) qm = jax.vmap(jp.dot)(a_i, b_j)
# add diagonal
if d is not None: if d is not None:
qm = qm.at[m.dof_Madr].add(d) diag_adr = m.M_rowadr + m.M_rownnz - 1
qm = qm.at[diag_adr].add(d)
return qm return qm
def full_m(m: Model, d: Data) -> jax.Array: def full_m(m: Model, d: Data) -> jax.Array:
"""Reconstitute dense mass matrix from qM.""" """Reconstitute dense mass matrix from M."""
if not is_sparse(m): if not is_sparse(m):
return d._impl.qM # pytype: disable=attribute-error return d._impl.M # pytype: disable=attribute-error
ij = [] i = np.repeat(np.arange(m.nv), m.M_rownnz)
for i in range(m.nv): j = m.M_colind
j = i
while j > -1:
ij.append((i, j))
j = m.dof_parentid[j]
i, j = (jp.array(x) for x in zip(*ij)) mat = jp.zeros((m.nv, m.nv)).at[(i, j)].set(d._impl.M) # pytype: disable=attribute-error
mat = jp.zeros((m.nv, m.nv)).at[(i, j)].set(d._impl.qM) # pytype: disable=attribute-error
# also set upper triangular # also set upper triangular
mat = mat + jp.tril(mat, -1).T mat = mat + jp.tril(mat, -1).T
@@ -116,24 +104,23 @@ def mul_m(m: Model, d: Data, vec: jax.Array) -> jax.Array:
"""Multiply vector by inertia matrix.""" """Multiply vector by inertia matrix."""
if not is_sparse(m): if not is_sparse(m):
return d._impl.qM @ vec # pytype: disable=attribute-error return d._impl.M @ vec # pytype: disable=attribute-error
diag_mul = d._impl.qM[jp.array(m.dof_Madr)] * vec # pytype: disable=attribute-error diag_adr = m.M_rowadr + m.M_rownnz - 1
diag_mul = d._impl.M[diag_adr] * vec # pytype: disable=attribute-error
is_, js, madr_ijs = [], [], [] is_, js, madr_ijs = [], [], []
for i in range(m.nv): for i in range(m.nv):
madr_ij, j = m.dof_Madr[i], i adr = m.M_rowadr[i]
for k in range(m.M_rownnz[i] - 1):
is_.append(i)
js.append(m.M_colind[adr + k])
madr_ijs.append(adr + k)
while True: i, j, madr_ij = (np.array(x, dtype=np.int32) for x in (is_, js, madr_ijs))
madr_ij, j = madr_ij + 1, m.dof_parentid[j]
if j == -1:
break
is_, js, madr_ijs = is_ + [i], js + [j], madr_ijs + [madr_ij]
i, j, madr_ij = (jp.array(x, dtype=jp.int32) for x in (is_, js, madr_ijs)) out = diag_mul.at[i].add(d._impl.M[madr_ij] * vec[j]) # pytype: disable=attribute-error
out = out.at[j].add(d._impl.M[madr_ij] * vec[i]) # pytype: disable=attribute-error
out = diag_mul.at[i].add(d._impl.qM[madr_ij] * vec[j]) # pytype: disable=attribute-error
out = out.at[j].add(d._impl.qM[madr_ij] * vec[i]) # pytype: disable=attribute-error
return out return out
+3 -1
View File
@@ -696,6 +696,9 @@ class Model(PyTreeNode):
dof_treeid: np.ndarray dof_treeid: np.ndarray
dof_Madr: np.ndarray # pylint:disable=invalid-name dof_Madr: np.ndarray # pylint:disable=invalid-name
dof_simplenum: np.ndarray dof_simplenum: np.ndarray
M_rowadr: np.ndarray # pylint:disable=invalid-name
M_rownnz: np.ndarray # pylint:disable=invalid-name
M_colind: np.ndarray # pylint:disable=invalid-name
dof_solref: jax.Array dof_solref: jax.Array
dof_solimp: jax.Array dof_solimp: jax.Array
dof_frictionloss: jax.Array dof_frictionloss: jax.Array
@@ -996,7 +999,6 @@ class DataJAX(PyTreeNode):
wrap_xpos: jax.Array wrap_xpos: jax.Array
actuator_moment: jax.Array actuator_moment: jax.Array
crb: jax.Array crb: jax.Array
qM: jax.Array # pylint:disable=invalid-name
M: jax.Array # pylint:disable=invalid-name M: jax.Array # pylint:disable=invalid-name
qLD: jax.Array # pylint:disable=invalid-name qLD: jax.Array # pylint:disable=invalid-name
qLDiagInv: jax.Array # pylint:disable=invalid-name qLDiagInv: jax.Array # pylint:disable=invalid-name