Add mju_sym2dense, document future breakage of mj_fullM
PiperOrigin-RevId: 910242375 Change-Id: Ibfbdef9cfb66088723499ea257da09aee0d80938
This commit is contained in:
committed by
Copybara-Service
parent
a51a7bf062
commit
767c607f58
@@ -3982,6 +3982,15 @@ Convert matrix from dense to sparse.
|
||||
|
||||
Convert matrix from sparse to dense.
|
||||
|
||||
.. _mju_sym2dense:
|
||||
|
||||
`mju_sym2dense <#mju_sym2dense>`__
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. mujoco-include:: mju_sym2dense
|
||||
|
||||
Convert lower-triangular symmetric CSR matrix to full dense matrix.
|
||||
|
||||
.. _Quaternions:
|
||||
|
||||
Quaternions
|
||||
|
||||
@@ -5,6 +5,8 @@ Changelog
|
||||
Upcoming version (not yet released)
|
||||
-----------------------------------
|
||||
|
||||
General
|
||||
^^^^^^^
|
||||
- Added island support for the :ref:`PGS solver<soAlgorithms>`.
|
||||
- Added support for :ref:`elastic2d<body-flexcomp-elastic2d>` for trilinear and quadratic flex
|
||||
:ref:`dofs<body-flexcomp-dof>`.
|
||||
@@ -13,6 +15,18 @@ Upcoming version (not yet released)
|
||||
(nonzero :ref:`density<option-density>` or :ref:`viscosity<option-viscosity>`).
|
||||
Midpoint integration treats external forces as zero-order-hold constants, which causes
|
||||
energy gain in the presence of contacts and in fluid media.
|
||||
- Added :ref:`mju_sym2dense`, converting a lower-triangular, implicitly symmetric CSR matrix to a dense
|
||||
symmetric matrix. The inertia matrix ``mjData.M`` is an example of such a matrix.
|
||||
|
||||
.. admonition:: Future breaking API changes
|
||||
:class: warning
|
||||
|
||||
- The introduction of :ref:`mju_sym2dense` is a step towards the removal of the legacy-format ``mjData.qM`` in favor
|
||||
of the CSR-format ``mjData.M``. This removal will involve a future breaking change to :ref:`mj_fullM` (which
|
||||
currently accepts a ``qM``-like matrix as an argument). To prevent a future breakage, replace
|
||||
``mj_fullM(m, dst, d->qM)`` with
|
||||
|br| ``mju_sym2dense(dst, d->M, m->nv, m->M_rownnz, m->M_rowadr, m->M_colind)``.
|
||||
|
||||
|
||||
Python
|
||||
^^^^^^
|
||||
|
||||
@@ -3522,6 +3522,8 @@ int mju_dense2sparse(mjtNum* res, const mjtNum* mat, int nr, int nc,
|
||||
int* rownnz, int* rowadr, int* colind, int nnz);
|
||||
void mju_sparse2dense(mjtNum* res, const mjtNum* mat, int nr, int nc,
|
||||
const int* rownnz, const int* rowadr, const int* colind);
|
||||
void mju_sym2dense(mjtNum* res, const mjtNum* mat, int n,
|
||||
const int* rownnz, const int* rowadr, const int* colind);
|
||||
void mju_rotVecQuat(mjtNum res[3], const mjtNum vec[3], const mjtNum quat[4]);
|
||||
void mju_negQuat(mjtNum res[4], const mjtNum quat[4]);
|
||||
void mju_mulQuat(mjtNum res[4], const mjtNum quat1[4], const mjtNum quat2[4]);
|
||||
|
||||
@@ -1206,6 +1206,10 @@ MJAPI int mju_dense2sparse(mjtNum* res, const mjtNum* mat, int nr, int nc,
|
||||
MJAPI void mju_sparse2dense(mjtNum* res, const mjtNum* mat, int nr, int nc,
|
||||
const int* rownnz, const int* rowadr, const int* colind);
|
||||
|
||||
// Convert lower-triangular symmetric CSR matrix to full dense matrix.
|
||||
MJAPI void mju_sym2dense(mjtNum* res, const mjtNum* mat, int n,
|
||||
const int* rownnz, const int* rowadr, const int* colind);
|
||||
|
||||
|
||||
//---------------------------------- Quaternions ---------------------------------------------------
|
||||
|
||||
|
||||
@@ -1058,7 +1058,13 @@ def _put_data_jax(
|
||||
# convert qM and qLD if jacobian is dense
|
||||
if not support.is_sparse(m):
|
||||
impl_fields['qM'] = np.zeros((m.nv, m.nv))
|
||||
mujoco.mj_fullM(m, impl_fields['qM'], d.qM)
|
||||
mujoco.mju_sym2dense(
|
||||
impl_fields['qM'],
|
||||
d.M,
|
||||
m.M_rownnz,
|
||||
m.M_rowadr,
|
||||
m.M_colind,
|
||||
)
|
||||
# TODO(erikfrey): derive L*L' from L'*D*L instead of recomputing
|
||||
try:
|
||||
impl_fields['qLD'], _ = scipy.linalg.cho_factor(impl_fields['qM'])
|
||||
|
||||
@@ -520,7 +520,7 @@ class DataIOTest(parameterized.TestCase):
|
||||
if impl == 'jax':
|
||||
# check that qM is transformed properly
|
||||
qm = np.zeros((m.nv, m.nv), dtype=np.float64)
|
||||
mujoco.mj_fullM(m, qm, d.qM)
|
||||
mujoco.mju_sym2dense(qm, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
|
||||
np.testing.assert_allclose(qm, mjx.full_m(mjx.put_model(m), dx))
|
||||
|
||||
elif impl == 'cpp':
|
||||
@@ -529,7 +529,7 @@ class DataIOTest(parameterized.TestCase):
|
||||
return # cpp does not populate other fields in _impl
|
||||
elif impl == 'warp':
|
||||
qm = np.zeros((m.nv, m.nv), dtype=np.float64)
|
||||
mujoco.mj_fullM(m, qm, d.qM)
|
||||
mujoco.mju_sym2dense(qm, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
|
||||
np.testing.assert_allclose(dx._impl.qM, qm)
|
||||
# TODO(taylorhowell): test efc__J
|
||||
np.testing.assert_allclose(dx._impl.efc__aref[:3], d.efc_aref[:3])
|
||||
@@ -596,7 +596,7 @@ class DataIOTest(parameterized.TestCase):
|
||||
dx_from_dense = mjx.put_data(m, d, impl=impl)
|
||||
if impl == 'jax':
|
||||
qm = np.zeros((m.nv, m.nv))
|
||||
mujoco.mj_fullM(m, qm, d.qM)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -438,7 +438,7 @@ class TendonTest(parameterized.TestCase):
|
||||
|
||||
if jacobian == JacobianType.DENSE:
|
||||
qM = np.zeros((m.nv, m.nv)) # pylint: disable=invalid-name
|
||||
mujoco.mj_fullM(m, qM, d.qM)
|
||||
mujoco.mju_sym2dense(qM, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
|
||||
else:
|
||||
qM = d.qM # pylint: disable=invalid-name
|
||||
_assert_eq(dx._impl.qM, qM, 'qM')
|
||||
|
||||
@@ -64,7 +64,7 @@ class SupportTest(parameterized.TestCase):
|
||||
dx = mjx.put_data(m, d)
|
||||
mjx_full_m = jax.jit(support.full_m)(mx, dx)
|
||||
mj_full_m = np.zeros((m.nv, m.nv), dtype=np.float64)
|
||||
mujoco.mj_fullM(m, mj_full_m, d.qM)
|
||||
mujoco.mju_sym2dense(mj_full_m, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
|
||||
np.testing.assert_allclose(mjx_full_m, mj_full_m, atol=5e-5, rtol=5e-5)
|
||||
|
||||
@parameterized.parameters('constraints.xml', 'pendula.xml')
|
||||
|
||||
@@ -175,7 +175,7 @@ class ForwardTest(parameterized.TestCase):
|
||||
tu.assert_attr_eq(dx._impl, d, 'crb')
|
||||
|
||||
qm = np.zeros((m.nv, m.nv))
|
||||
mujoco.mj_fullM(m, qm, d.qM)
|
||||
mujoco.mju_sym2dense(qm, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
|
||||
# mjwarp adds padding to qM
|
||||
tu.assert_eq(qm, dx._impl.qM[: m.nv, : m.nv], 'qM')
|
||||
# qLD is fused in a cholesky factorize and solve, and not written to.
|
||||
|
||||
@@ -1241,6 +1241,37 @@ PYBIND11_MODULE(_functions, pymodule) {
|
||||
colind.data());
|
||||
});
|
||||
|
||||
DEF_WITH_OMITTED_PY_ARGS(traits::mju_sym2dense, "n")(
|
||||
pymodule,
|
||||
[](Eigen::Ref<EigenArrayXX> res,
|
||||
Eigen::Ref<const EigenVectorX> mat,
|
||||
Eigen::Ref<const EigenVectorI> rownnz,
|
||||
Eigen::Ref<const EigenVectorI> rowadr,
|
||||
Eigen::Ref<const EigenVectorI> colind) {
|
||||
if (res.rows() != res.cols()) {
|
||||
throw py::type_error("res should be a square matrix");
|
||||
}
|
||||
if (res.rows() != rownnz.size()) {
|
||||
throw py::type_error("#rows in res should equal size of rownnz");
|
||||
}
|
||||
if (res.rows() != rowadr.size()) {
|
||||
throw py::type_error("#rows in res should equal size of rowadr");
|
||||
}
|
||||
if (res.rows() > 0) {
|
||||
int nnz = rowadr.array().tail(1)[0] + rownnz.array().tail(1)[0];
|
||||
if (mat.size() < nnz) {
|
||||
throw py::type_error("mat size is too small for the given sparse "
|
||||
"structure");
|
||||
}
|
||||
if (colind.size() < nnz) {
|
||||
throw py::type_error("colind size is too small for the given "
|
||||
"sparse structure");
|
||||
}
|
||||
}
|
||||
return ::mju_sym2dense(res.data(), mat.data(), res.rows(),
|
||||
rownnz.data(), rowadr.data(), colind.data());
|
||||
});
|
||||
|
||||
// Quaternions
|
||||
Def<traits::mju_rotVecQuat>(pymodule);
|
||||
Def<traits::mju_negQuat>(pymodule);
|
||||
|
||||
@@ -7755,6 +7755,48 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
|
||||
),
|
||||
doc='Convert matrix from sparse to dense.',
|
||||
)),
|
||||
('mju_sym2dense',
|
||||
FunctionDecl(
|
||||
name='mju_sym2dense',
|
||||
return_type=ValueType(name='void'),
|
||||
parameters=(
|
||||
FunctionParameterDecl(
|
||||
name='res',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtNum'),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='mat',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='mjtNum', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='n',
|
||||
type=ValueType(name='int'),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='rownnz',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='int', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='rowadr',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='int', is_const=True),
|
||||
),
|
||||
),
|
||||
FunctionParameterDecl(
|
||||
name='colind',
|
||||
type=PointerType(
|
||||
inner_type=ValueType(name='int', is_const=True),
|
||||
),
|
||||
),
|
||||
),
|
||||
doc='Convert lower-triangular symmetric CSR matrix to full dense matrix.', # pylint: disable=line-too-long
|
||||
)),
|
||||
('mju_rotVecQuat',
|
||||
FunctionDecl(
|
||||
name='mju_rotVecQuat',
|
||||
|
||||
@@ -525,7 +525,7 @@ void mj_island(const mjModel* m, mjData* d) {
|
||||
d->island_dofadr[i] = d->map_idof2dof[d->island_idofadr[i]];
|
||||
}
|
||||
|
||||
// inertia: block-diagonalize both iLD <- qLD and iM <- qM
|
||||
// inertia: block-diagonalize both iLD <- qLD and iM <- M
|
||||
mju_blockDiagSparse(d->iLD, d->iM_rownnz, d->iM_rowadr, d->iM_colind,
|
||||
d->qLD, m->M_rownnz, m->M_rowadr, m->M_colind,
|
||||
nidof, nisland,
|
||||
|
||||
@@ -1307,7 +1307,7 @@ static void setStat(mjModel* m, mjData* d) {
|
||||
if (m->nv) {
|
||||
m->stat.meaninertia = 0;
|
||||
for (int i=0; i < m->nv; i++) {
|
||||
m->stat.meaninertia += d->qM[m->dof_Madr[i]];
|
||||
m->stat.meaninertia += d->M[m->M_rowadr[i] + m->M_rownnz[i] - 1];
|
||||
}
|
||||
m->stat.meaninertia /= m->nv;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,23 @@ void mju_sparse2dense(mjtNum* res, const mjtNum* mat, int nr, int nc,
|
||||
}
|
||||
|
||||
|
||||
// convert lower-triangular symmetric CSR matrix to full dense matrix
|
||||
void mju_sym2dense(mjtNum* res, const mjtNum* mat, int n,
|
||||
const int* rownnz, const int* rowadr, const int* colind) {
|
||||
mju_zero(res, n*n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
int adr = rowadr[i];
|
||||
for (int j = 0; j < rownnz[i]; j++) {
|
||||
int col = colind[adr+j];
|
||||
if (col <= i) {
|
||||
res[i*n+col] = mat[adr+j];
|
||||
res[col*n+i] = mat[adr+j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// res[row, :] = mat[row, :]
|
||||
void mju_copySparse(mjtNum* res, const mjtNum* mat, const int* rownnz, const int* rowadr,
|
||||
const int* row, int nrow) {
|
||||
|
||||
@@ -46,6 +46,10 @@ MJAPI int mju_dense2sparse(mjtNum* res, const mjtNum* mat, int nr, int nc,
|
||||
MJAPI void mju_sparse2dense(mjtNum* res, const mjtNum* mat, int nr, int nc, const int* rownnz,
|
||||
const int* rowadr, const int* colind);
|
||||
|
||||
// convert lower-triangular symmetric CSR matrix to full dense matrix
|
||||
MJAPI void mju_sym2dense(mjtNum* res, const mjtNum* mat, int n,
|
||||
const int* rownnz, const int* rowadr, const int* colind);
|
||||
|
||||
// res[row, :] = mat[row, :]
|
||||
void mju_copySparse(mjtNum* res, const mjtNum* mat, const int* rownnz, const int* rowadr,
|
||||
const int* row, int nrow);
|
||||
|
||||
@@ -548,7 +548,8 @@ TEST_F(InertiaTest, mulM) {
|
||||
|
||||
// dense M matrix
|
||||
vector<mjtNum> Mdense(nv*nv);
|
||||
mj_fullM(model, Mdense.data(), data->qM);
|
||||
mju_sym2dense(Mdense.data(), data->M, nv,
|
||||
model->M_rownnz, model->M_rowadr, model->M_colind);
|
||||
|
||||
// arbitrary RHS vector
|
||||
vector<mjtNum> vec(nv);
|
||||
@@ -611,9 +612,9 @@ TEST_F(InertiaTest, FullM) {
|
||||
mjData* d = mj_makeData(m);
|
||||
mj_forward(m, d);
|
||||
|
||||
// get dense mass matrix from qM using mj_fullM
|
||||
// get dense mass matrix from M using mju_sym2dense
|
||||
vector<mjtNum> M(nv * nv);
|
||||
mj_fullM(m, M.data(), d->qM);
|
||||
mju_sym2dense(M.data(), d->M, nv, m->M_rownnz, m->M_rowadr, m->M_colind);
|
||||
|
||||
// get dense mass matrix from M using mju_sparse2dense
|
||||
vector<mjtNum> M_CSR(nv * nv);
|
||||
|
||||
@@ -361,6 +361,42 @@ TEST_F(EngineUtilSparseTest, MjuCompressSparse) {
|
||||
EXPECT_EQ(AsVector(dense, 6), AsVector(dense_expected_minval1, 6));
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSym2Dense) {
|
||||
// lower-triangular CSR for a 3x3 symmetric matrix:
|
||||
// 1 2 0
|
||||
// 2 3 4
|
||||
// 0 4 5
|
||||
// stored as lower triangle:
|
||||
// row 0: [1] (col 0)
|
||||
// row 1: [2, 3] (cols 0, 1)
|
||||
// row 2: [4, 5] (cols 1, 2)
|
||||
mjtNum mat[] = {1, 2, 3, 4, 5};
|
||||
int rownnz[] = {1, 2, 2};
|
||||
int rowadr[] = {0, 1, 3};
|
||||
int colind[] = {0, 0, 1, 1, 2};
|
||||
|
||||
mjtNum dense[9];
|
||||
mju_sym2dense(dense, mat, 3, rownnz, rowadr, colind);
|
||||
|
||||
mjtNum expected[] = {1, 2, 0, 2, 3, 4, 0, 4, 5};
|
||||
EXPECT_EQ(AsVector(dense, 9), AsVector(expected, 9));
|
||||
}
|
||||
|
||||
TEST_F(EngineUtilSparseTest, MjuSym2DenseWithUpper) {
|
||||
mjtNum mat[] = {1, 999, 2, 3, 4, 5};
|
||||
int rownnz[] = {2, 2, 2};
|
||||
int rowadr[] = {0, 2, 4};
|
||||
int colind[] = {0, 1, 0, 1, 1, 2};
|
||||
|
||||
mjtNum dense[9];
|
||||
mju_sym2dense(dense, mat, 3, rownnz, rowadr, colind);
|
||||
|
||||
mjtNum expected[] = {1, 2, 0,
|
||||
2, 3, 4,
|
||||
0, 4, 5};
|
||||
EXPECT_EQ(AsVector(dense, 9), AsVector(expected, 9));
|
||||
}
|
||||
|
||||
// helper: run split-col approach and return dense result
|
||||
static void SqrMatTDSplitCol(
|
||||
std::vector<mjtNum>& dense_result, int nr, int nc,
|
||||
|
||||
@@ -7514,6 +7514,9 @@ public static unsafe extern int mju_dense2sparse(double* res, double* mat, int n
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_sparse2dense(double* res, double* mat, int nr, int nc, int* rownnz, int* rowadr, int* colind);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_sym2dense(double* res, double* mat, int n, int* rownnz, int* rowadr, int* colind);
|
||||
|
||||
[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static unsafe extern void mju_rotVecQuat(double* res, double* vec, double* quat);
|
||||
|
||||
|
||||
@@ -10638,6 +10638,15 @@ mjtNum mju_sum_wrapper(const NumberArray& vec, int n) {
|
||||
return mju_sum(vec_.data(), n);
|
||||
}
|
||||
|
||||
void mju_sym2dense_wrapper(const val& res, const NumberArray& mat, int n, const NumberArray& rownnz, const NumberArray& rowadr, const NumberArray& colind) {
|
||||
UNPACK_VALUE(mjtNum, res);
|
||||
UNPACK_ARRAY(mjtNum, mat);
|
||||
UNPACK_ARRAY(int, rownnz);
|
||||
UNPACK_ARRAY(int, rowadr);
|
||||
UNPACK_ARRAY(int, colind);
|
||||
mju_sym2dense(res_.data(), mat_.data(), n, rownnz_.data(), rowadr_.data(), colind_.data());
|
||||
}
|
||||
|
||||
void mju_symmetrize_wrapper(const val& res, const NumberArray& mat, int n) {
|
||||
UNPACK_VALUE(mjtNum, res);
|
||||
UNPACK_ARRAY(mjtNum, mat);
|
||||
@@ -13461,6 +13470,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
|
||||
function("mju_subFrom3", &mju_subFrom3_wrapper);
|
||||
function("mju_subQuat", &mju_subQuat_wrapper);
|
||||
function("mju_sum", &mju_sum_wrapper);
|
||||
function("mju_sym2dense", &mju_sym2dense_wrapper);
|
||||
function("mju_symmetrize", &mju_symmetrize_wrapper);
|
||||
function("mju_transformSpatial", &mju_transformSpatial_wrapper);
|
||||
function("mju_transpose", &mju_transpose_wrapper);
|
||||
|
||||
Reference in New Issue
Block a user