Files
Mujoco_WASM/src/engine/engine_util_sparse.c
T
Yuval Tassa a2d0e33c0f 2-3x speedup of sparse matrix squaring.
Split symbolic and numeric phases for sparse `M'*diag*M` computation. Microseconds per call for the monolithic vs the split approach for the 100_humanoids and 2humanoid100 models:

```
+-------+------+----------+------------+---------+
| Model | Arch | Col (µs) | Split (µs) | Speedup |
+-------+------+----------+------------+---------+
| 2H100 | x86  | 238.3    | 74.5       | 3.2x    |
+-------+------+----------+------------+---------+
|       | ARM  | 111.6    | 53.2       | 2.1x    |
+-------+------+----------+------------+---------+
| 100H  | x86  | 1325.3   | 656.2      | 2.0x    |
+-------+------+----------+------------+---------+
|       | ARM  | 594.8    | 306.6      | 1.9x    |
+-------+------+----------+------------+---------+
```

PiperOrigin-RevId: 900154308
Change-Id: Ia6e9b8e196e2ed37b723a0faf60e9731303a9619
2026-04-15 07:19:41 -07:00

1477 lines
40 KiB
C

// 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.
#include "engine/engine_util_sparse.h"
#include <mujoco/mjdata.h>
#include <mujoco/mjmacro.h>
#include <mujoco/mjsan.h> // IWYU pragma: keep
#include <mujoco/mjtnum.h>
#include "engine/engine_memory.h"
#include "engine/engine_util_blas.h"
#include "engine/engine_util_misc.h"
#include "engine/engine_util_sparse_avx.h" // IWYU pragma: keep
//------------------------------ sparse operations -------------------------------------------------
// dot-productX3, first vector is sparse; supernode of size 3
void mju_dotSparseX3(mjtNum* res0, mjtNum* res1, mjtNum* res2,
const mjtNum* vec10, const mjtNum* vec11, const mjtNum* vec12,
const mjtNum* vec2, int nnz1, const int* ind1) {
#ifdef mjUSEAVX
mju_dotSparseX3_avx(res0, res1, res2, vec10, vec11, vec12, vec2, nnz1, ind1);
#else
int i = 0;
// clear result
mjtNum RES0 = 0;
mjtNum RES1 = 0;
mjtNum RES2 = 0;
for (; i < nnz1; i++) {
mjtNum v2 = vec2[ind1[i]];
RES0 += vec10[i] * v2;
RES1 += vec11[i] * v2;
RES2 += vec12[i] * v2;
}
// copy result
*res0 = RES0;
*res1 = RES1;
*res2 = RES2;
#endif // mjUSEAVX
}
// dot-product, both vectors are sparse
mjtNum mju_dotSparse2(const mjtNum* vec1, const int* ind1, int nnz1,
const mjtNum* vec2, const int* ind2, int nnz2) {
int i1 = 0, i2 = 0;
mjtNum res = 0;
// check for empty array
if (!nnz1 || !nnz2) {
return 0;
}
while (i1 < nnz1 && i2 < nnz2) {
// get current indices
int adr1 = ind1[i1], adr2 = ind2[i2];
// match: accumulate result, advance both
if (adr1 == adr2) {
res += vec1[i1++] * vec2[i2++];
}
// otherwise advance smaller
else if (adr1 < adr2) {
i1++;
} else {
i2++;
}
}
return res;
}
// convert matrix from dense to sparse
// nnz is size of res and colind, return 1 if too small, 0 otherwise
int mju_dense2sparse(mjtNum* res, const mjtNum* mat, int nr, int nc,
int* rownnz, int* rowadr, int* colind, int nnz) {
if (nnz <= 0) {
return 1;
}
int adr = 0;
// find non-zeros and construct sparse
for (int r=0; r < nr; r++) {
// init row
rownnz[r] = 0;
rowadr[r] = adr;
// find non-zeros
for (int c=0; c < nc; c++) {
if (mat[r*nc+c]) {
// check for out of bounds
if (adr >= nnz) {
return 1;
}
// record index and count
colind[adr] = c;
rownnz[r]++;
// copy element
res[adr++] = mat[r*nc+c];
}
}
}
return 0;
}
// convert matrix from sparse to dense
void mju_sparse2dense(mjtNum* res, const mjtNum* mat, int nr, int nc,
const int* rownnz, const int* rowadr, const int* colind) {
// clear
mju_zero(res, nr*nc);
// copy non-zeros
for (int r=0; r < nr; r++) {
for (int i=0; i < rownnz[r]; i++) {
res[r*nc + colind[rowadr[r]+i]] = mat[rowadr[r]+i];
}
}
}
// res[row, :] = mat[row, :]
void mju_copySparse(mjtNum* res, const mjtNum* mat, const int* rownnz, const int* rowadr,
const int* row, int nrow) {
for (int i=0; i < nrow; i++) {
int r = row[i];
mju_copy(res + rowadr[r], mat + rowadr[r], rownnz[r]);
}
}
// res[row, :] = 0
void mju_zeroSparse(mjtNum* res, const int* rownnz, const int* rowadr, const int* row, int nrow) {
for (int i=0; i < nrow; i++) {
int r = row[i];
mju_zero(res + rowadr[r], rownnz[r]);
}
}
// multiply sparse matrix and dense vector: res = mat * vec.
void mju_mulMatVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec,
int nr, const int* rownnz, const int* rowadr,
const int* colind, const int* rowsuper) {
#ifdef mjUSEAVX
mju_mulMatVecSparse_avx(res, mat, vec, nr, rownnz, rowadr, colind, rowsuper);
#else
// regular sparse dot-product
for (int r=0; r < nr; r++) {
res[r] = mju_dotSparse(mat+rowadr[r], vec, rownnz[r], colind+rowadr[r]);
}
#endif // mjUSEAVX
}
// multiply transposed sparse matrix and dense vector: res = mat' * vec.
void mju_mulMatTVecSparse(mjtNum* res, const mjtNum* mat, const mjtNum* vec, int nr, int nc,
const int* rownnz, const int* rowadr, const int* colind) {
// clear res
mju_zero(res, nc);
for (int i=0; i < nr; i++) {
mjtNum scl = vec[i];
// skip if 0
if (!scl) continue;
// add row scaled by the corresponding vector element
int nnz = rownnz[i];
int adr = rowadr[i];
const int* ind = colind + adr;
const mjtNum* row = mat + adr;
for (int j=0; j < nnz; j++) {
res[ind[j]] += row[j] * scl;
}
}
}
// add sparse matrix M to sparse destination matrix, requires pre-allocated buffers
void mju_addToMatSparse(mjtNum* dst, int* rownnz, int* rowadr, int* colind, int nr,
const mjtNum* M, const int* M_rownnz, const int* M_rowadr,
const int* M_colind,
mjtNum* buf_val, int* buf_ind) {
for (int i=0; i < nr; i++) {
rownnz[i] = mju_combineSparse(dst + rowadr[i], M + M_rowadr[i], 1, 1,
rownnz[i], M_rownnz[i], colind + rowadr[i],
M_colind + M_rowadr[i], buf_val, buf_ind);
}
}
// add symmetric matrix (lower triangle) to dense matrix, upper triangle optional
void mju_addToSymSparse(mjtNum* res, const mjtNum* mat, int n,
const int* rownnz, const int* rowadr, const int* colind, int flg_upper) {
for (int i=0; i < n; i++) {
int start = rowadr[i];
int end = start + rownnz[i];
for (int adr=start; adr < end; adr++) {
mjtNum val = mat[adr];
int j = colind[adr];
// lower + diagonal
res[i*n + j] += val;
// strict upper
if (flg_upper && j < i) {
res[j*n + i] += val;
}
}
}
}
// multiply symmetric matrix (only lower triangle represented) by vector:
// res = (mat + strict_upper(mat')) * vec
void mju_mulSymVecSparse(mjtNum* restrict res, const mjtNum* restrict mat,
const mjtNum* restrict vec, int n,
const int* restrict rownnz, const int* restrict rowadr,
const int* restrict colind) {
// clear res
mju_zero(res, n);
// multiply
for (int i=0; i < n; i++) {
int adr = rowadr[i];
int diag = rownnz[i] - 1;
const mjtNum* row = mat + adr;
// diagonal
res[i] = row[diag] * vec[i];
// off-diagonals
const int* ind = colind + adr;
for (int k=diag-1; k >= 0; k--) {
int j = ind[k];
mjtNum val = row[k];
res[i] += val * vec[j]; // strict lower
res[j] += val * vec[i]; // strict upper
}
}
}
// count the number of non-zeros in the sum of two sparse vectors
int mju_combineSparseCount(int a_nnz, int b_nnz, const int* a_ind, const int* b_ind) {
int a = 0, b = 0, c_nnz = 0;
// count c_nnz: nonzero indices common to both a and b
while (a < a_nnz && b < b_nnz) {
// common index, increment everything
if (a_ind[a] == b_ind[b]) {
c_nnz++;
a++;
b++;
}
// update smallest index
else if (a_ind[a] < b_ind[b]) {
a++;
} else {
b++;
}
}
// union minus the intersection
return a_nnz + b_nnz - c_nnz;
}
// incomplete combine sparse: dst = a*dst + b*src at common indices
void mju_combineSparseInc(mjtNum* dst, const mjtNum* src, int n, mjtNum a, mjtNum b,
int dst_nnz, int src_nnz, const int* dst_ind, const int* src_ind) {
// check for identical pattern
if (dst_nnz == src_nnz) {
if (mju_compare(dst_ind, src_ind, dst_nnz)) {
// combine mjtNum data directly
mju_addToSclScl(dst, src, a, b, dst_nnz);
return;
}
}
// scale dst by a
if (a != 1) {
mju_scl(dst, dst, a, dst_nnz);
}
// prepare to merge
int di = 0, si = 0;
int dadr = di < dst_nnz ? dst_ind[di] : n+1;
int sadr = si < src_nnz ? src_ind[si] : n+1;
// add src*b at common indices
while (di < dst_nnz) {
// both
if (dadr == sadr) {
dst[di++] += b*src[si++];
dadr = di < dst_nnz ? dst_ind[di] : n+1;
sadr = si < src_nnz ? src_ind[si] : n+1;
}
// dst only
else if (dadr < sadr) {
di++;
dadr = di < dst_nnz ? dst_ind[di] : n+1;
}
// src only
else {
si++;
sadr = si < src_nnz ? src_ind[si] : n+1;
}
}
}
// dst += scl*src, only at common non-zero indices
void mju_addToSclSparseInc(mjtNum* restrict dst, const mjtNum* restrict src,
int nnzdst, const int* restrict inddst,
int nnzsrc, const int* restrict indsrc, mjtNum scl) {
if (!nnzdst || !nnzsrc) {
return;
}
int adrs = 0, adrd = 0;
while (adrs < nnzsrc && adrd < nnzdst) {
int inds = indsrc[adrs];
int indd = inddst[adrd];
if (inds == indd) {
dst[adrd] += scl * src[adrs];
adrs++;
adrd++;
} else if (inds < indd) {
adrs++;
} else {
adrd++;
}
}
}
// add to sparse matrix: dst = dst + scl*src, return nnz of result
int mju_addToSparseMat(mjtNum* dst, const mjtNum* src, int n, int nrow, mjtNum scl,
int dst_nnz, int src_nnz, int* dst_ind, const int* src_ind,
mjtNum* buf, int* buf_ind) {
// check for identical pattern
if (dst_nnz == src_nnz) {
if (dst_nnz == 0) {
return 0;
}
if (mju_compare(dst_ind, src_ind, dst_nnz)) {
// combine mjtNum data directly
mju_addToScl(dst, src, scl, nrow*dst_nnz);
return dst_nnz;
}
}
// prepare to merge scr and dst into buf^T
int si = 0, di = 0, nnz = 0;
int sadr = src_nnz ? src_ind[0] : n+1;
int dadr = dst_nnz ? dst_ind[0] : n+1;
// merge matrices
while (si < src_nnz || di < dst_nnz) {
// both
if (sadr == dadr) {
for (int k=0; k < nrow; k++) {
buf[nrow*nnz + k] = dst[di + k*dst_nnz] + scl*src[si + k*src_nnz];
}
buf_ind[nnz++] = sadr;
si++;
di++;
sadr = si < src_nnz ? src_ind[si] : n+1;
dadr = di < dst_nnz ? dst_ind[di] : n+1;
}
// dst only
else if (dadr < sadr) {
for (int k=0; k < nrow; k++) {
buf[nrow*nnz + k] = dst[di + k*dst_nnz];
}
buf_ind[nnz++] = dadr;
di++;
dadr = di < dst_nnz ? dst_ind[di] : n+1;
}
// src only
else {
for (int k=0; k < nrow; k++) {
buf[nrow*nnz + k] = scl*src[si + k*src_nnz];
}
buf_ind[nnz++] = sadr;
si++;
sadr = si < src_nnz ? src_ind[si] : n+1;
}
}
// copy transposed buf into dst
mju_transpose(dst, buf, nnz, nrow);
mju_copyInt(dst_ind, buf_ind, nnz);
return nnz;
}
// add(merge) two chains
int mju_addChains(int* res, int n, int NV1, int NV2,
const int* chain1, const int* chain2) {
// check for identical pattern
if (NV1 == NV2) {
if (NV1 == 0) {
return 0;
}
if (mju_compare(chain1, chain2, NV1)) {
mju_copyInt(res, chain1, NV1);
return NV1;
}
}
// prepare to merge
int i1 = 0, i2 = 0, NV = 0;
int adr1 = NV1 ? chain1[0] : n+1;
int adr2 = NV2 ? chain2[0] : n+1;
// merge chains
while (i1 < NV1 || i2 < NV2) {
// both
if (adr1 == adr2) {
res[NV++] = adr1;
i1++;
i2++;
adr1 = i1 < NV1 ? chain1[i1] : n+1;
adr2 = i2 < NV2 ? chain2[i2] : n+1;
}
// chain1 only
else if (adr1 < adr2) {
res[NV++] = adr1;
i1++;
adr1 = i1 < NV1 ? chain1[i1] : n+1;
}
// chain2 only
else {
res[NV++] = adr2;
i2++;
adr2 = i2 < NV2 ? chain2[i2] : n+1;
}
}
return NV;
}
// compress sparse matrix, remove elements with abs(value) <= minval, return total non-zeros
int mju_compressSparse(mjtNum* mat, int nr, int nc, int* rownnz, int* rowadr, int* colind,
mjtNum minval) {
int remove_small = minval >= 0;
int adr = 0;
for (int r=0; r < nr; r++) {
// save old rowadr, record new
int rowadr_old = rowadr[r];
rowadr[r] = adr;
// shift mat and colind
int nnz = 0;
int end = rowadr_old + rownnz[r];
for (int adr_old=rowadr_old; adr_old < end; adr_old++) {
if (remove_small && mju_abs(mat[adr_old]) <= minval) {
continue;
}
if (adr != adr_old) {
mat[adr] = mat[adr_old];
colind[adr] = colind[adr_old];
}
adr++;
if (remove_small) nnz++;
}
if (remove_small) rownnz[r] = nnz;
}
return rowadr[nr-1] + rownnz[nr-1];
}
// transpose sparse matrix, optionally compute row supernodes
void mju_transposeSparse(mjtNum* res, const mjtNum* mat, int nr, int nc,
int* res_rownnz, int* res_rowadr, int* res_colind, int* res_rowsuper,
const int* rownnz, const int* rowadr, const int* colind) {
if (!nr || !nc) return;
// clear number of non-zeros for each row of transposed
mju_zeroInt(res_rownnz, nc);
// handle the case where the first row of mat is nonzero (offset wrt the base pointers)
int row_offset = rowadr[0];
// count the number of non-zeros for each row of the transposed matrix
for (int r = 0; r < nr; r++) {
int start = rowadr[r] - row_offset;
int end = start + rownnz[r];
for (int j = start; j < end; j++) {
res_rownnz[colind[j]]++;
}
}
// init res_rowsuper
if (res_rowsuper) {
for (int i = 0; i < nc - 1; i++) {
res_rowsuper[i] = (res_rownnz[i] == res_rownnz[i + 1]);
}
res_rowsuper[nc - 1] = 0;
}
// compute the row addresses for the transposed matrix
res_rowadr[0] = 0;
for (int i = 1; i < nc; i++) {
res_rowadr[i] = res_rowadr[i-1] + res_rownnz[i-1];
}
// iterate through each row (column) of mat (res)
for (int r = 0; r < nr; r++) {
int c_prev = -1;
int start = rowadr[r] - row_offset;
int end = start + rownnz[r];
for (int i = start; i < end; i++) {
// swap rows with columns and increment res_rowadr
int c = colind[i];
int adr = res_rowadr[c]++;
res_colind[adr] = r;
if (res) {
res[adr] = mat[i];
}
// mark non-supernodes
if (res_rowsuper) {
if (c > 0 && c != c_prev + 1 && res_rowsuper[c - 1]) {
res_rowsuper[c - 1] = 0;
}
c_prev = c;
}
}
}
// shift back row addresses
for (int i = nc-1; i > 0; i--) {
res_rowadr[i] = res_rowadr[i-1];
}
res_rowadr[0] = 0;
// accumulate supernodes
if (res_rowsuper) {
for (int i = nc - 2; i >= 0; i--) {
if (res_rowsuper[i]) {
res_rowsuper[i] += res_rowsuper[i + 1];
}
}
}
}
// construct row supernodes
void mju_superSparse(int nr, int* rowsuper,
const int* rownnz, const int* rowadr, const int* colind) {
// no rows: nothing to do
if (!nr) {
return;
}
// find match to child
for (int r=0; r < nr-1; r++) {
// different number of nonzeros: cannot be a match
if (rownnz[r] != rownnz[r+1]) {
rowsuper[r] = 0;
}
// same number of nonzeros: compare colind vectors
else {
rowsuper[r] = mju_compare(colind+rowadr[r], colind+rowadr[r+1], rownnz[r]);
}
}
// clear last (by definition)
rowsuper[nr-1] = 0;
// accumulate in reverse
for (int r=nr-2; r >= 0; r--) {
if (rowsuper[r]) {
rowsuper[r] += rowsuper[r+1];
}
}
}
// precount res_rownnz and precompute res_rowadr for mju_sqrMatTDSparse, return total non-zeros
int mju_sqrMatTDSparseCount(int* res_rownnz, int* res_rowadr, int nr,
const int* rownnz, const int* rowadr, const int* colind,
const int* rownnzT, const int* rowadrT, const int* colindT,
const int* rowsuperT, mjData* d, int flg_upper) {
mj_markStack(d);
int* chain = mjSTACKALLOC(d, 2*nr, int);
int nchain = 0;
int* res_colind = NULL;
for (int r=0; r < nr; r++) {
// supernode; copy everything to next row
if (rowsuperT && r > 0 && rowsuperT[r-1] > 0) {
res_rownnz[r] = res_rownnz[r - 1];
// fill in upper triangle
if (flg_upper) {
for (int j=0; j < nchain; j++) {
res_rownnz[res_colind[j]]++;
}
}
// update chain with diagonal
if (rownnzT[r]) {
res_colind[nchain++] = r;
res_rownnz[r]++;
}
} else {
int inew = 0, iold = nr;
nchain = 0;
for (int i=0; i < rownnzT[r]; i++) {
int c = colindT[rowadrT[r] + i];
int adr = inew;
inew = iold;
iold = adr;
int nnewchain = 0;
adr = 0;
int end = rowadr[c] + rownnz[c];
for (int adr1=rowadr[c]; adr1 < end; adr1++) {
int col_mat = colind[adr1];
while (adr < nchain && chain[iold + adr] < col_mat && chain[iold + adr] <= r) {
chain[inew + nnewchain++] = chain[iold + adr++];
}
// skip upper triangle
if (col_mat > r) {
break;
}
if (adr < nchain && chain[iold + adr] == col_mat) {
adr++;
}
chain[inew + nnewchain++] = col_mat;
}
while (adr < nchain && chain[iold + adr] <= r) {
chain[inew + nnewchain++] = chain[iold + adr++];
}
nchain = nnewchain;
}
// only computed for lower triangle
res_rownnz[r] = nchain;
res_colind = chain + inew;
// update upper triangle
if (flg_upper) {
int nchain_end = nchain;
// avoid double counting
if (nchain > 0 && res_colind[nchain-1] == r) {
nchain_end = nchain - 1;
}
for (int j=0; j < nchain_end; j++) {
res_rownnz[res_colind[j]]++;
}
}
}
}
res_rowadr[0] = 0;
for (int r = 1; r < nr; r++) {
res_rowadr[r] = res_rowadr[r-1] + res_rownnz[r-1];
}
mj_freeStack(d);
return res_rowadr[nr-1] + res_rownnz[nr-1];
}
// precompute res_rowadr for mju_sqrMatTDSparse using uncompressed memory
void mju_sqrMatTDUncompressedInit(int* res_rowadr, int nc) {
for (int r=0; r < nc; r++) {
res_rowadr[r] = r*nc;
}
}
// max number of supernodes handled by column-based matrix squaring functions
#define mjMAXSUPER 8
// column-based symbolic phase for sparse matrix squaring: compute sparsity pattern of M'*M
// if res_colind is NULL: count mode, fill res_rownnz/res_rowadr, return nnz
// if res_colind is not NULL: fill mode, write sorted column indices
// if res_diagind is not NULL: also fill upper triangle and output diagonal indices
int mju_sqrMatTDSparseSymbolic(
int* restrict res_rownnz, int* restrict res_rowadr,
int* restrict res_colind, int* restrict res_diagind, int nr, int nc,
const int* rownnz, const int* rowadr, const int* colind,
const int* rownnzT, const int* rowadrT, const int* colindT, const int* rowsuperT, mjData* d) {
mj_markStack(d);
// reinterpret M^T as CSC
const int* colnnz = rownnzT;
const int* coladr = rowadrT;
const int* rowind = colindT;
const int* colsuper = rowsuperT;
// reinterpret M as CSC
const int* colnnzT = rownnz;
const int* coladrT = rowadr;
const int* rowindT = colind;
// marker[j] = 1 if row j has been visited in current column batch
int* marker = mjSTACKALLOC(d, nc, int);
mju_zeroInt(marker, nc);
// buffer_idx: list of row indices with nonzeros in current column batch
int* buffer_idx = mjSTACKALLOC(d, nc, int);
// rowstart[r]: first index in row r of M where column > current result column
int* rowstart = mjSTACKALLOC(d, nr, int);
mju_zeroInt(rowstart, nr);
// clear res_rownnz (used for both counting and filling)
mju_zeroInt(res_rownnz, nc);
// process result columns c = 0, 1, ..., nc-1
int ns; // set in the loop
for (int c = 0; c < nc; c += ns) {
int buffer_nnz = 0;
// column c of M^T
int nnz_c = colnnz[c];
int adr_c = coladr[c];
const int* ind_c = rowind + adr_c;
// supernode size: how many consecutive columns share the same sparsity pattern
ns = 1;
int cs;
if (colsuper && (cs = colsuper[c])) {
ns += mjMIN(cs, mjMAXSUPER - 1);
}
// for each row r where M^T[r, c] != 0, look at row r of M
for (int i = 0; i < nnz_c; i++) {
int r = ind_c[i];
int adrT = coladrT[r];
int nnzT = colnnzT[r];
const int* indT = rowindT + adrT;
// scan row r of M, starting from rowstart[r]
for (int k = rowstart[r]; k < nnzT; k++) {
int j = indT[k];
// skip if j <= c: only fill the strict lower triangle
if (j <= c) {
rowstart[r]++;
continue;
}
// new nonzero in row j of result
if (!marker[j]) {
marker[j] = 1;
buffer_idx[buffer_nnz++] = j;
}
}
}
// scatter: update result rows j > c that have nonzeros in this column batch
// fill mode: write column indices, clear markers
if (res_colind) {
for (int i = 0; i < buffer_nnz; i++) {
int j = buffer_idx[i];
marker[j] = 0;
int nm = mjMIN(ns, j - c);
int adr_j = res_rowadr[j] + res_rownnz[j];
for (int s = 0; s < nm; s++) {
res_colind[adr_j + s] = c + s;
}
res_rownnz[j] += nm;
}
// write diagonal entries
for (int s = 0; s < ns; s++) {
int col = c + s;
if (colnnz[col]) {
res_colind[res_rowadr[col] + res_rownnz[col]] = col;
res_rownnz[col]++;
}
}
}
// count mode: just count and clear markers
else {
for (int i = 0; i < buffer_nnz; i++) {
int j = buffer_idx[i];
marker[j] = 0;
int nm = mjMIN(ns, j - c);
res_rownnz[j] += nm;
if (res_diagind) {
for (int s = 0; s < nm; s++) {
res_rownnz[c + s]++;
}
}
}
// count diagonal entries
for (int s = 0; s < ns; s++) {
int col = c + s;
if (colnnz[col]) {
res_rownnz[col]++;
}
}
}
}
// count mode: compute res_rowadr from res_rownnz
if (!res_colind) {
res_rowadr[0] = 0;
for (int r = 1; r < nc; r++) {
res_rowadr[r] = res_rowadr[r - 1] + res_rownnz[r - 1];
}
}
// fill mode with upper triangle: record diagonal positions and mirror from lower
if (res_colind && res_diagind) {
// save current counts (lower + diagonal)
int* lower_nnz = mjSTACKALLOC(d, nc, int);
mju_copyInt(lower_nnz, res_rownnz, nc);
// save diagonal indices
for (int r = 0; r < nc; r++) {
res_diagind[r] = res_rowadr[r] + lower_nnz[r] - 1;
}
// fill upper triangle: for each (r, c) with c < r, write to (c, r)
for (int r = 0; r < nc; r++) {
int adr = res_rowadr[r];
int nnz = lower_nnz[r];
for (int j = 0; j < nnz; j++) {
int col = res_colind[adr + j];
if (col < r) {
res_colind[res_rowadr[col] + res_rownnz[col]++] = r;
}
}
}
}
mj_freeStack(d);
return res_rowadr[nc - 1] + res_rownnz[nc - 1];
}
// numeric phase for sparse matrix squaring: compute values given pre-computed sparsity
// diagind can be NULL, otherwise fills upper triangle and saves diagonal indices
void mju_sqrMatTDSparseNumeric(
mjtNum* restrict res, int nc,
const int* res_rownnz, const int* res_rowadr, const int* res_colind, const int* res_diagind,
const mjtNum* mat, const int* rownnz, const int* rowadr, const int* colind,
const mjtNum* matT, const int* rownnzT, const int* rowadrT, const int* colindT,
const int* rowsuperT, const mjtNum* diag, mjData* d) {
mj_markStack(d);
// dense accumulator for current result row (or batch of rows)
mjtNum* restrict buffer = mjSTACKALLOC(d, nc * mjMAXSUPER, mjtNum);
mju_zero(buffer, nc * mjMAXSUPER);
// process result rows
int ns; // set in the loop
for (int r = 0; r < nc; r += ns) {
// determine supernode size
ns = 1;
if (rowsuperT) {
ns = rowsuperT[r] + 1;
if (ns > mjMAXSUPER) ns = mjMAXSUPER;
}
// single row
if (ns == 1) {
int nnzT_r = rownnzT[r];
int adr_r = rowadrT[r];
// accumulate: res[r, :] = sum over k in M'[r, :] of diag[k] * M'[r, k] * M[k, :]
for (int i = 0; i < nnzT_r; i++) {
int k = colindT[adr_r + i];
mjtNum valT = matT[adr_r + i];
mjtNum scale = diag ? diag[k] * valT : valT;
if (scale == 0) continue;
int adr_k = rowadr[k];
int nnz_k = rownnz[k];
const int* ind_k = colind + adr_k;
const mjtNum* val_k = mat + adr_k;
for (int j = 0; j < nnz_k; j++) {
int c = ind_k[j];
if (c > r) break;
buffer[c] += scale * val_k[j];
}
}
// scatter from dense buffer to sparse result
int res_adr = res_rowadr[r];
int res_nnz = res_rownnz[r];
const int* res_ind = res_colind + res_adr;
mjtNum* res_val = res + res_adr;
for (int j = 0; j < res_nnz; j++) {
int c = res_ind[j];
res_val[j] = buffer[c];
buffer[c] = 0;
}
}
// supernode: ns > 1 rows share the same sparsity pattern
else {
int nnzT_r = rownnzT[r];
int adr_r = rowadrT[r];
// accumulate for ns rows
for (int i = 0; i < nnzT_r; i++) {
int k = colindT[adr_r + i];
// compute scale for all rows
mjtNum scale[mjMAXSUPER];
if (diag) {
mjtNum dk = diag[k];
if (dk == 0) continue;
for (int s = 0; s < ns; s++) {
scale[s] = dk * matT[rowadrT[r + s] + i];
}
} else {
for (int s = 0; s < ns; s++) {
scale[s] = matT[rowadrT[r + s] + i];
}
}
int adr_k = rowadr[k];
int nnz_k = rownnz[k];
const int* ind_k = colind + adr_k;
const mjtNum* val_k = mat + adr_k;
for (int j = 0; j < nnz_k; j++) {
int c = ind_k[j];
if (c > r + ns - 1) break; // skip if beyond block
mjtNum v = val_k[j];
for (int s = 0; s < ns; s++) {
if (c <= r + s) {
buffer[s * nc + c] += scale[s] * v;
}
}
}
}
// scatter
for (int s = 0; s < ns; s++) {
int row = r + s;
int res_adr = res_rowadr[row];
int res_nnz = res_rownnz[row];
const int* res_ind = res_colind + res_adr;
mjtNum* res_val = res + res_adr;
for (int j = 0; j < res_nnz; j++) {
int c = res_ind[j];
res_val[j] = buffer[s*nc + c];
buffer[s*nc + c] = 0;
}
}
}
}
// fill upper triangle: mirror values from lower triangle
if (res_diagind) {
// initialize write positions after diagonal
int* upper_pos = mjSTACKALLOC(d, nc, int);
for (int r = 0; r < nc; r++) {
upper_pos[r] = res_diagind[r] + 1;
}
// for each (r, c) with c < r, write r to row c
for (int r = 0; r < nc; r++) {
int adr = res_rowadr[r];
int lower_nnz = res_diagind[r] - adr + 1;
for (int j = 0; j < lower_nnz; j++) {
int c = res_colind[adr + j];
if (c < r) {
res[upper_pos[c]++] = res[adr + j];
}
}
}
}
mj_freeStack(d);
}
// compute sparse M'*diag*M (diag=NULL: compute M'*M), res_rowadr must be precomputed
void mju_sqrMatTDSparse(mjtNum* res, const mjtNum* mat, const mjtNum* matT,
const mjtNum* diag, int nr, int nc,
int* res_rownnz, const int* res_rowadr, int* res_colind,
const int* rownnz, const int* rowadr,
const int* colind, const int* rowsuper,
const int* rownnzT, const int* rowadrT,
const int* colindT, const int* rowsuperT,
mjData* d, int* diagind) {
mj_markStack(d);
// reinterpret transposed matrices as compressed sparse column
const mjtNum* mat_csc = matT;
const int* colnnz = rownnzT;
const int* coladr = rowadrT;
const int* rowind = colindT;
const int* colsuper = rowsuperT;
const mjtNum* matT_csc = mat;
const int* colnnzT = rownnz;
const int* coladrT = rowadr;
const int* rowindT = colind;
// rowsuper is unused
// marker[i] = 1 if row i is set in current column
int* marker = mjSTACKALLOC(d, nc, int);
mju_zeroInt(marker, nc);
// dense buffer (considered column-major) containing up to mjMAXSUPER columns
mjtNum* buffer = mjSTACKALLOC(d, nc*mjMAXSUPER, mjtNum);
// dense index vector of the current column (unsorted)
int* buffer_idx = mjSTACKALLOC(d, nc, int);
// rowstart[i]: address of first row in column mat'[:, i] with index > current column
int* rowstart = mjSTACKALLOC(d, nr, int);
mju_zeroInt(rowstart, nr);
// clear res_rownnz
mju_zeroInt(res_rownnz, nc);
// construct res[lower+diagonal], by column
for (int c=0; c < nc; c++) {
int buffer_nnz = 0;
// prepare column c of mat
int nnz = colnnz[c];
int adr = coladr[c];
const int* ind = rowind + adr;
// val: array of ns > 0 column pointers with identical pattern to c
const mjtNum* val[mjMAXSUPER];
// first column is c
int ns = 1;
val[0] = mat_csc + adr;
// add c's supernodes, if any
int cs;
if (colsuper && (cs = colsuper[c])) {
ns += mjMIN(cs, mjMAXSUPER - 1);
for (int s=1; s < ns; s++) {
val[s] = mat_csc + coladr[c + s];
}
}
// diagonal special-case: dense dot product of column c, with/out diag
mjtNum diag_c[mjMAXSUPER];
if (diag) {
for (int s=0; s < ns; s++) {
mjtNum ds = 0;
for (int k=0; k < nnz; k++) {
ds += (val[s][k] * val[s][k]) * diag[ind[k]];
}
diag_c[s] = ds;
}
} else {
for (int s=0; s < ns; s++) {
diag_c[s] = mju_dot(val[s], val[s], nnz);
}
}
// in the strict lower triangle, compute
// res[:, c] = mat' * mat[:, c] = sum_r(diag[r] * mat'[:, r] * mat[:, c])
for (int i=0; i < nnz; i++) {
// prepare column r of mat'
int r = ind[i];
int adrT = coladrT[r];
int nnzT = colnnzT[r];
const int* indT = rowindT + adrT;
const mjtNum* valT = matT_csc + adrT;
// get v[s] = diag[r] * mat[r, c + s] for s in [0, ns)
mjtNum v[mjMAXSUPER];
if (diag) {
mjtNum diag_r = diag[r];
for (int s=0; s < ns; s++) {
v[s] = diag_r * val[s][i];
}
} else {
for (int s=0; s < ns; s++) {
v[s] = val[s][i];
}
}
// gather to dense buffer columns: buffer[:, s] += mat'[:, r] * v[s]
for (int k=rowstart[r]; k < nnzT; k++) {
int j = indT[k];
// if j is not in the strict lower triangle, increment rowstart and continue
if (j <= c) {
rowstart[r]++;
continue;
}
// first nonzero in row j: mark and set value
if (!marker[j]) {
// mark j and save it
marker[j] = 1;
buffer_idx[buffer_nnz++] = j;
// set value
mjtNum vk = valT[k];
for (int s=0; s < ns; s++) {
buffer[s*nc + j] = vk * v[s];
}
}
// otherwise existing nonzero in row j: add to value
else {
mjtNum vk = valT[k];
for (int s=0; s < ns; s++) {
buffer[s*nc + j] += vk * v[s];
}
}
}
}
// scatter to res from dense buffer: res[:, c + s] = buffer[:, s] for s in [0, ns)
// write values under diagonal
for (int i=0; i < buffer_nnz; i++) {
int j = buffer_idx[i];
marker[j] = 0;
int adr_j = res_rowadr[j] + res_rownnz[j];
// truncate row to strict lower triangle
int lower = j - c;
int nm = mjMIN(ns, lower);
// increment nonzeros
res_rownnz[j] += nm;
// write value
for (int s=0; s < nm; s++) {
res[adr_j + s] = buffer[s*nc + j];
}
// write index
for (int s=0; s < nm; s++) {
res_colind[adr_j + s] = c + s;
}
}
// write diagonal value
for (int s=0; s < ns; s++) {
int adr_s = res_rowadr[c + s] + res_rownnz[c + s]++;
res_colind[adr_s] = c + s;
res[adr_s] = diag_c[s];
}
// supernode: skip ahead if ns > 1
c += ns - 1;
}
// upper triangle requested: save diagonal indices and fill
if (diagind) {
// save diagonal indices
for (int i=0; i < nc; i++) {
diagind[i] = res_rowadr[i] + res_rownnz[i] - 1;
}
// fill upper triangle
for (int i=0; i < nc; i++) {
int start = res_rowadr[i];
int end = start + res_rownnz[i] - 1;
for (int j=start; j < end; j++) {
int adr = res_rowadr[res_colind[j]] + res_rownnz[res_colind[j]]++;
res[adr] = res[j];
res_colind[adr] = i;
}
}
}
mj_freeStack(d);
}
#undef mjMAXSUPER
// legacy row-based implementation (reference)
void mju_sqrMatTDSparse_row(mjtNum* res, const mjtNum* mat, const mjtNum* matT,
const mjtNum* diag, int nr, int nc,
int* res_rownnz, const int* res_rowadr, int* res_colind,
const int* rownnz, const int* rowadr,
const int* colind, const int* rowsuper,
const int* rownnzT, const int* rowadrT,
const int* colindT, const int* rowsuperT,
mjData* d, int* diagind) {
// allocate space for accumulation buffer and matT
mj_markStack(d);
// a dense row buffer that stores the current row in the resulting matrix
mjtNum* buffer = mjSTACKALLOC(d, nc, mjtNum);
// these mark the currently set columns in the dense row buffer,
// used for when creating the resulting sparse row
int* markers = mjSTACKALLOC(d, nc, int);
for (int i=0; i < nc; i++) {
int rowadr_i = res_rowadr[i];
int* cols = res_colind + rowadr_i;
res_rownnz[i] = 0;
buffer[i] = 0;
markers[i] = 0;
// if rowsuper, use the previous row sparsity structure
if (rowsuperT && i > 0 && rowsuperT[i-1]) {
res_rownnz[i] = res_rownnz[i-1];
mju_copyInt(cols, res_colind+res_rowadr[i-1], res_rownnz[i]);
}
// iterate through each row of M'
int adrT = rowadrT[i];
int end_r = adrT + rownnzT[i];
for (int r = adrT; r < end_r; r++) {
int t = colindT[r];
int adr = rowadr[t];
int end_c = adr + rownnz[t];
for (int c=adr; c < end_c; c++) {
int cc = colind[c];
// ignore upper triangle
if (cc > i) {
break;
}
// add value to buffer
if (diag) {
buffer[cc] += matT[r] * diag[t] * mat[c];
} else {
buffer[cc] += matT[r] * mat[c];
}
// only need to insert nnz if not marked
if (!markers[cc]) {
markers[cc] = 1;
// since i is the rightmost column, it can be inserted at the end
if (cc == i) {
cols[res_rownnz[i]++] = cc;
continue;
}
// insert col in order via binary search
int l = 0, h = res_rownnz[i];
while (l < h) {
int m = (l + h) >> 1;
if (cols[m] < cc) {
l = m + 1;
} else {
h = m;
}
}
// cc is the rightmost column so far, it can be inserted at the end
if (l == res_rownnz[i]) {
cols[l] = cc;
res_rownnz[i]++;
continue;
}
// move the cols to the right
h = res_rownnz[i];
while (l < h) {
cols[h] = cols[h-1];
h--;
}
// insert
cols[l] = cc;
res_rownnz[i]++;
}
}
}
end_r = res_rownnz[i];
// rowsuperT: reuse sparsity, copy into res
if (rowsuperT && rowsuperT[i]) {
for (int r=0; r < end_r; r++) {
int c = cols[r];
res[rowadr_i + r] = buffer[c];
buffer[c] = 0;
}
}
// clear out buffers, sparsity cannot be reused
else {
for (int r=0; r < end_r; r++) {
int c = cols[r];
int adr = rowadr_i + r;
res[adr] = buffer[c];
res_colind[adr] = c;
buffer[c] = 0;
markers[c] = 0;
}
}
}
// diagonal indices requested: fill upper triangle
if (diagind) {
// save diagonal indices
for (int i=0; i < nc; i++) {
diagind[i] = res_rowadr[i] + res_rownnz[i] - 1;
}
// fill upper triangle
for (int i=0; i < nc; i++) {
int start = res_rowadr[i];
int end = start + res_rownnz[i] - 1;
for (int j=start; j < end; j++) {
int adr = res_rowadr[res_colind[j]] + res_rownnz[res_colind[j]]++;
res[adr] = res[j];
res_colind[adr] = i;
}
}
}
mj_freeStack(d);
}
// block-diagonalize a dense matrix
// res output matrix
// mat input matrix
// nc_mat number of columns in mat
// nc_res number of columns in res
// nb number of blocks
// perm_r reverse permutation of rows (res -> mat)
// perm_c reverse permutation of columns (res -> mat)
// block_nr number of rows in each block
// block_nc number of columns in each block
// block_r first row of each block
// block_c first column of each block
void mju_blockDiag(mjtNum* restrict res, const mjtNum* restrict mat,
int nc_mat, int nc_res, int nb,
const int* restrict perm_r, const int* restrict perm_c,
const int* restrict block_nr, const int* restrict block_nc,
const int* restrict block_r, const int* restrict block_c) {
for (int b=0; b < nb; b++) {
int bnr = block_nr[b];
int bnc = block_nc[b];
const int* adr_r = perm_r + block_r[b];
const int* adr_c = perm_c + block_c[b];
int adr = nc_res * block_r[b];
for (int r = 0; r < bnr; r++) {
for (int c = 0; c < bnc; c++) {
res[adr++] = mat[nc_mat * adr_r[r] + adr_c[c]];
}
}
}
}
// block-diagonalize a sparse matrix
// res values of the target matrix res
// res_rownnz number of non-zeros in each row of res
// res_rowadr row address of each non-zero in res
// res_colind column index of each non-zero in res
// mat values of the source matrix mat
// mat_rownnz number of non-zeros in each row of mat
// mat_rowadr row address of each non-zero in mat
// mat_colind column index of each non-zero in mat
// nr number of rows in mat/res
// nb number of blocks
// perm_r reverse permutation of rows (res -> mat)
// perm_c forward permutation of columns (mat -> res)
// block_r first row of each block in res
// block_c first column of each block in res
// mat2 optional additional source matrix (same structure as mat)
// res2 optional additional target matrix (same structure as res)
void mju_blockDiagSparse(mjtNum* restrict res, int* restrict res_rownnz,
int* restrict res_rowadr, int* restrict res_colind,
const mjtNum* restrict mat, const int* restrict rownnz,
const int* restrict rowadr, const int* restrict colind,
int nr, int nb,
const int* restrict perm_r, const int* restrict perm_c,
const int* restrict block_r, const int* restrict block_c,
mjtNum* restrict res2, const mjtNum* restrict mat2) {
int block = 0;
int col_offset = block_c[block];
int row_next = block + 1 < nb ? block_r[block + 1] : nr;
for (int r=0; r < nr; r++) {
// row k in mat goes to row r in res
int k = perm_r[r];
// rownnz
int nnz = rownnz[k];
res_rownnz[r] = nnz;
// rowadr
int res_adr = (r == 0) ? 0 : res_rowadr[r-1] + res_rownnz[r-1];
res_rowadr[r] = res_adr;
// colind
int* res_colind_r = res_colind + res_adr;
mjtNum* res_r = res + res_adr;
int mat_adr = rowadr[k];
const int* colind_k = colind + mat_adr;
const mjtNum* mat_k = mat + mat_adr;
for (int j=0; j < nnz; j++) {
res_colind_r[j] = perm_c[colind_k[j]] - col_offset;
}
// values (dense copy: partial order within block is guaranteed)
mju_copy(res_r, mat_k, nnz);
if (mat2 && res2) {
mju_copy(res2 + res_adr, mat2 + mat_adr, nnz);
}
// end of block reached: update block counter, column offset, next row
if (r + 1 >= row_next && block + 1 < nb) {
block++;
col_offset = block_c[block];
row_next = block + 1 < nb ? block_r[block + 1] : nr;
}
}
}