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
This commit is contained in:
Yuval Tassa
2026-04-15 07:19:03 -07:00
committed by Copybara-Service
parent 62cffb1536
commit a2d0e33c0f
8 changed files with 1301 additions and 626 deletions
+428 -372
View File
@@ -14,10 +14,11 @@
// Tests for engine/engine_util_sparse.c
#include <array>
#include "src/engine/engine_util_sparse.h"
#include <array>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mujoco/mujoco.h>
@@ -360,6 +361,45 @@ TEST_F(EngineUtilSparseTest, MjuCompressSparse) {
EXPECT_EQ(AsVector(dense, 6), AsVector(dense_expected_minval1, 6));
}
// helper: run split-col approach and return dense result
static void SqrMatTDSplitCol(
std::vector<mjtNum>& dense_result, int nr, int nc,
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,
int* out_diagind, mjData* d) {
// count mode
std::vector<int> H_rownnz(nc, 0);
std::vector<int> H_rowadr(nc, 0);
int nnz = mju_sqrMatTDSparseSymbolic(
H_rownnz.data(), H_rowadr.data(), nullptr,
out_diagind, nr, nc, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, d);
// fill mode
std::vector<int> H_colind(nnz);
mju_sqrMatTDSparseSymbolic(
H_rownnz.data(), H_rowadr.data(), H_colind.data(),
out_diagind, nr, nc, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, d);
// numeric phase
std::vector<mjtNum> H(nnz, 0);
mju_sqrMatTDSparseNumeric(
H.data(), nc, H_rownnz.data(), H_rowadr.data(),
H_colind.data(), out_diagind, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, rowsuperT, diag, d);
// densify
dense_result.assign(nc * nc, 0);
for (int r = 0; r < nc; r++) {
for (int j = 0; j < H_rownnz[r]; j++) {
int c = H_colind[H_rowadr[r] + j];
dense_result[r*nc + c] = H[H_rowadr[r] + j];
}
}
}
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse1) {
// 0 0 0
// M = 0 0 0
@@ -378,29 +418,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse1) {
int rownnzT[] = {3, 3, 3};
int rowadrT[] = {0, 3, 6};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
diagindH, data);
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(0, 0, 0, 0, 0, 0, 0, 0, 0));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(0, 0, 0, 0, 0, 0, 0, 0, 0));
mj_deleteData(data);
mj_deleteModel(model);
@@ -424,27 +448,12 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseLower) {
int rownnzT[] = {3, 3, 3};
int rowadrT[] = {0, 3, 6};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
nullptr, data);
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 0);
EXPECT_THAT(rownnzH, ElementsAre(1, 2, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 1, 3));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, nullptr);
EXPECT_THAT(matH, ElementsAre(12, 0, 0, 0, 6, 0, 12, 3, 14));
EXPECT_THAT(colindH, ElementsAre(0, 0, 0, 0, 1, 0, 0, 1, 2));
EXPECT_THAT(rownnzH, ElementsAre(1, 2, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(12, 0, 0, 0, 6, 0, 12, 3, 14));
mj_deleteData(data);
mj_deleteModel(model);
@@ -468,31 +477,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse2) {
int rownnzT[] = {3, 3, 3};
int rowadrT[] = {0, 3, 6};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
diagindH, data);
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(12, 0, 12, 0, 6, 3, 12, 3, 14));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(diagindH, ElementsAre(0, 4, 8));
EXPECT_THAT(dense, ElementsAre(12, 0, 12, 0, 6, 3, 12, 3, 14));
mj_deleteData(data);
mj_deleteModel(model);
@@ -516,31 +507,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3) {
int rownnzT[] = {2, 2, 0};
int rowadrT[] = {0, 2, 4};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {2, 3, 4};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 0));
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 4));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(66, 4, 0, 4, 35, 0, 0, 0, 0));
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 0, 2, 0, 0));
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 1));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(66, 4, 0, 4, 35, 0, 0, 0, 0));
mj_deleteData(data);
mj_deleteModel(model);
@@ -564,32 +539,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse3b) {
int rownnzT[] = {2, 2, 1};
int rowadrT[] = {0, 2, 4};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {1, 1, 1};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(2, 3, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 5));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(26, 2, 0, 2, 13, 12, 12, 16, 0));
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 1, 2, 1, 2, 0));
EXPECT_THAT(rownnzH, ElementsAre(2, 3, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(diagindH, ElementsAre(0, 4, 7));
EXPECT_THAT(dense, ElementsAre(26, 2, 0, 2, 13, 12, 0, 12, 16));
mj_deleteData(data);
mj_deleteModel(model);
@@ -613,32 +571,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse4) {
int rownnzT[] = {2, 0, 2};
int rowadrT[] = {0, 2, 2};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {2, 3, 4};
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, diag,
diagindH, data);
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
EXPECT_THAT(rownnzH, ElementsAre(2, 0, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 2));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(66, 4, 0, 0, 0, 0, 4, 35, 0));
EXPECT_THAT(colindH, ElementsAre(0, 2, 0, 1, 0, 0, 0, 2, 0));
EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(66, 0, 4, 0, 0, 0, 4, 0, 35));
mj_deleteData(data);
mj_deleteModel(model);
@@ -662,30 +603,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse5) {
int rownnzT[] = {2, 1, 1};
int rowadrT[] = {0, 2, 3};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
diagindH, data);
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 5));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(5, 6, 4, 6, 9, 0, 4, 16, 0));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 0, 0, 2, 0));
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(5, 6, 4, 6, 9, 0, 4, 0, 16));
mj_deleteData(data);
mj_deleteModel(model);
@@ -709,30 +633,13 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse6) {
int rownnzT[] = {1, 1, 2};
int rowadrT[] = {0, 1, 2};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, nullptr,
diagindH, data);
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 3));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(1, 2, 0, 4, 0, 0, 2, 13, 0));
EXPECT_THAT(colindH, ElementsAre(0, 2, 0, 1, 0, 0, 0, 2, 0));
EXPECT_THAT(rownnzH, ElementsAre(2, 1, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(diagindH, ElementsAre(0, 3, 7));
EXPECT_THAT(dense, ElementsAre(1, 0, 2, 0, 4, 0, 2, 0, 13));
mj_deleteData(data);
mj_deleteModel(model);
@@ -756,31 +663,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse7) {
int rownnzT[] = {2, 2};
int rowadrT[] = {0, 2};
mjtNum matH[] = {0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0};
int rownnzH[] = {0, 0};
int rowadrH[] = {0, 0};
int diagindH[] = {0, 0};
mjtNum diag[] = {2, 3, 4};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 2, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
int diagindH[2];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 2, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(2, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 2));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 2);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 2, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(66, 4, 4, 35));
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 1));
EXPECT_THAT(rownnzH, ElementsAre(2, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 2));
EXPECT_THAT(dense, ElementsAre(66, 4, 4, 35));
mj_deleteData(data);
mj_deleteModel(model);
@@ -803,31 +694,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse8) {
int rownnzT[] = {2, 1, 1};
int rowadrT[] = {0, 2, 3};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {2, 3};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 2, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 5));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 2, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(14, 18, 8, 18, 27, 0, 8, 32, 0));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 0, 0, 2, 0));
EXPECT_THAT(rownnzH, ElementsAre(3, 2, 2));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(14, 18, 8, 18, 27, 0, 8, 0, 32));
mj_deleteData(data);
mj_deleteModel(model);
@@ -851,31 +726,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse9) {
int rownnzT[] = {3, 3, 3};
int rowadrT[] = {0, 3, 6};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {2, 3, 4};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, nullptr, data, 1);
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, nullptr, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
nullptr, data, diagindH);
EXPECT_THAT(matH, ElementsAre(69, 77, 80, 77, 99, 108, 80, 108, 120));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(69, 77, 80, 77, 99, 108, 80, 108, 120));
mj_deleteData(data);
mj_deleteModel(model);
@@ -900,31 +759,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse10) {
int rowadrT[] = {0, 3, 6};
int rowsuperT[] = {2, 1, 0};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {1, 2, 1};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
rowsuperT, data, diagindH);
EXPECT_THAT(matH, ElementsAre(18, 17, 14, 17, 23, 19, 14, 19, 18));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(18, 17, 14, 17, 23, 19, 14, 19, 18));
mj_deleteData(data);
mj_deleteModel(model);
@@ -949,31 +792,15 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse11) {
int rowadrT[] = {0, 1, 3};
int rowsuperT[] = {0, 1, 0};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0};
int rowadrH[] = {0, 0, 0};
int diagindH[] = {0, 0, 0};
mjtNum diag[] = {1, 1, 1};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 3, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
int diagindH[3];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 3, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 3);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 3, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
rowsuperT, data, diagindH);
EXPECT_THAT(matH, ElementsAre(1, 1, 1, 1, 10, 10, 1, 10, 10));
EXPECT_THAT(colindH, ElementsAre(0, 1, 2, 0, 1, 2, 0, 1, 2));
EXPECT_THAT(rownnzH, ElementsAre(3, 3, 3));
EXPECT_THAT(rowadrH, ElementsAre(0, 3, 6));
EXPECT_THAT(dense, ElementsAre(1, 1, 1, 1, 10, 10, 1, 10, 10));
mj_deleteData(data);
mj_deleteModel(model);
@@ -998,33 +825,16 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse12) {
int rowadrT[] = {0, 1, 2, 4};
int rowsuperT[] = {1, 0, 1, 0};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0, 0};
int rowadrH[] = {0, 0, 0, 0};
int diagindH[] = {0, 0, 0, 0};
mjtNum diag[] = {1, 1, 1};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 4, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
int diagindH[4];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 4, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(4, 4, 4, 4));
EXPECT_THAT(rowadrH, ElementsAre(0, 4, 8, 12));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 4);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 4, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
rowsuperT, data, diagindH);
EXPECT_THAT(matH,
EXPECT_THAT(dense,
ElementsAre(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 10, 10, 1, 1, 10, 10));
EXPECT_THAT(colindH,
ElementsAre(0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3));
EXPECT_THAT(rownnzH, ElementsAre(4, 4, 4, 4));
EXPECT_THAT(rowadrH, ElementsAre(0, 4, 8, 12));
mj_deleteData(data);
mj_deleteModel(model);
@@ -1049,35 +859,16 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse13) {
int rowadrT[] = {0, 3, 6, 6, 6};
int rowsuperT[] = {1, 0, 2, 1, 0};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0, 0, 0};
int rowadrH[] = {0, 0, 0, 0, 0};
int diagindH[] = {0, 0, 0, 0, 0};
mjtNum diag[] = {1, 1, 1};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 5, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
int diagindH[5];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 3, 5, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, rowsuperT, diag,
diagindH, data);
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 0, 0, 0));
EXPECT_THAT(rowadrH, ElementsAre(0, 2, 4, 4, 4));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 5);
mju_sqrMatTDSparse(matH, mat, matT, diag, 3, 5, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
rowsuperT, data, diagindH);
EXPECT_THAT(matH, ElementsAre(3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0));
EXPECT_THAT(colindH, ElementsAre(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0,
3, 0, 0, 0, 0, 4, 0, 0, 0, 0));
EXPECT_THAT(rownnzH, ElementsAre(2, 2, 1, 1, 1));
EXPECT_THAT(rowadrH, ElementsAre(0, 5, 10, 15, 20));
EXPECT_THAT(dense, ElementsAre(3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0));
mj_deleteData(data);
mj_deleteModel(model);
@@ -1100,40 +891,305 @@ TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparse14) {
int rowadrT[] = {0, 1, 2, 3, 4, 5, 6};
int rowsuperT[] = {3, 2, 1, 0, 2, 1, 0};
mjtNum matH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int colindH[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int rownnzH[] = {0, 0, 0, 0, 0, 0, 0};
int rowadrH[] = {0, 0, 0, 0, 0, 0, 0};
int diagindH[] = {0, 0, 0, 0, 0, 0, 0};
// test precount
mju_sqrMatTDSparseCount(rownnzH, rowadrH, 7, rownnz, rowadr, colind,
rownnzT, rowadrT, colindT, rowsuperT, data, 1);
EXPECT_THAT(rownnzH, ElementsAre(7, 7, 7, 7, 7, 7, 7));
EXPECT_THAT(rowadrH, ElementsAre(0, 7, 14, 21, 28, 35, 42));
// test computation
mju_sqrMatTDUncompressedInit(rowadrH, 7);
mju_sqrMatTDSparse(matH, mat, matT, nullptr, 1, 7, rownnzH, rowadrH, colindH,
rownnz, rowadr, colind, nullptr, rownnzT, rowadrT, colindT,
rowsuperT, data, diagindH);
int diagindH[7];
std::vector<mjtNum> dense;
SqrMatTDSplitCol(dense, 1, 7, mat, rownnz, rowadr, colind,
matT, rownnzT, rowadrT, colindT, rowsuperT, nullptr,
diagindH, data);
EXPECT_THAT(
matH, ElementsAre(1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2,
2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 4, 4, 4, 2, 2, 2,
2, 4, 4, 4, 2, 2, 2, 2, 4, 4, 4));
EXPECT_THAT(colindH,
ElementsAre(0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3,
4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 0,
1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6));
dense, ElementsAre(1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1,
1, 2, 2, 2, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 4, 4,
4, 2, 2, 2, 2, 4, 4, 4, 2, 2, 2, 2, 4, 4, 4));
EXPECT_THAT(rownnzH, ElementsAre(7, 7, 7, 7, 7, 7, 7));
EXPECT_THAT(rowadrH, ElementsAre(0, 7, 14, 21, 28, 35, 42));
mj_deleteData(data);
mj_deleteModel(model);
}
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseSymbolic) {
// Simple dense 2x2 matrix:
// 1 2
// M = 3 4
//
// M'M (lower triangle) should have 3 elements: (0,0), (1,0), (1,1)
mjModel* model = LoadModelFromString("<mujoco/>");
mjData* data = mj_makeData(model);
// M in CSR: row 0 has cols 0,1; row 1 has cols 0,1
int colind[] = {0, 1, 0, 1};
int rownnz[] = {2, 2};
int rowadr[] = {0, 2};
// compute transpose using mju_transposeSparse
mjtNum mat[] = {1, 2, 3, 4};
mjtNum matT[4];
int colindT[4];
int rownnzT[2];
int rowadrT[2];
mju_transposeSparse(matT, mat, 2, 2, rownnzT, rowadrT, colindT, nullptr,
rownnz, rowadr, colind);
// use old function as ground truth
int rownnzH_expected[] = {0, 0};
int rowadrH_expected[] = {0, 0};
int nnz_expected = mju_sqrMatTDSparseCount(
rownnzH_expected, rowadrH_expected, 2, rownnz, rowadr, colind, rownnzT,
rowadrT, colindT, nullptr, data, /*flg_upper=*/0);
// verify: lower triangle should have 3 elements: (0,0), (1,0), (1,1)
EXPECT_EQ(nnz_expected, 3);
EXPECT_THAT(rownnzH_expected, ElementsAre(1, 2));
EXPECT_THAT(rowadrH_expected, ElementsAre(0, 1));
// test count mode of new function
int rownnzH[] = {0, 0};
int rowadrH[] = {0, 0};
int nnz = mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, nullptr, nullptr,
2, 2, rownnz, rowadr, colind, rownnzT,
rowadrT, colindT, nullptr, data);
EXPECT_EQ(nnz, nnz_expected);
EXPECT_THAT(rownnzH, ElementsAre(rownnzH_expected[0], rownnzH_expected[1]));
EXPECT_THAT(rowadrH, ElementsAre(rowadrH_expected[0], rowadrH_expected[1]));
// test fill mode
std::vector<int> colindH(nnz, -1);
mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, colindH.data(), nullptr, 2, 2,
rownnz, rowadr, colind, rownnzT, rowadrT,
colindT, nullptr, data);
// verify: row 0 should have {0}, row 1 should have {0, 1}
EXPECT_THAT(colindH, ElementsAre(0, 0, 1));
mj_deleteData(data);
mj_deleteModel(model);
}
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseSymbolicUpper) {
// Test flg_upper=1: count both lower and upper triangle
// Same matrix as previous test
mjModel* model = LoadModelFromString("<mujoco/>");
mjData* data = mj_makeData(model);
int colind[] = {0, 1, 0, 1};
int rownnz[] = {2, 2};
int rowadr[] = {0, 2};
mjtNum mat[] = {1, 2, 3, 4};
mjtNum matT[4];
int colindT[4];
int rownnzT[2];
int rowadrT[2];
mju_transposeSparse(matT, mat, 2, 2, rownnzT, rowadrT, colindT, nullptr,
rownnz, rowadr, colind);
// use old function as ground truth with flg_upper=1
int rownnzH_expected[] = {0, 0};
int rowadrH_expected[] = {0, 0};
int nnz_expected = mju_sqrMatTDSparseCount(
rownnzH_expected, rowadrH_expected, 2, rownnz, rowadr, colind, rownnzT,
rowadrT, colindT, nullptr, data, /*flg_upper=*/1);
// test new function with diagind (upper triangle)
int rownnzH[] = {0, 0};
int rowadrH[] = {0, 0};
int diagindH[] = {0, 0};
int nnz = mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, nullptr, diagindH,
2, 2, rownnz, rowadr, colind, rownnzT,
rowadrT, colindT, nullptr, data);
EXPECT_EQ(nnz, nnz_expected);
EXPECT_THAT(rownnzH, ElementsAre(rownnzH_expected[0], rownnzH_expected[1]));
EXPECT_THAT(rowadrH, ElementsAre(rowadrH_expected[0], rowadrH_expected[1]));
mj_deleteData(data);
mj_deleteModel(model);
}
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseSymbolicSupernode) {
// Test supernode exploitation with a matrix that has supernodes
// M has two rows with identical sparsity pattern
mjModel* model = LoadModelFromString("<mujoco/>");
mjData* data = mj_makeData(model);
// 3x2 matrix where rows 1 and 2 have same pattern
// 1 0
// M = 2 3
// 4 5
int colind[] = {0, 0, 1, 0, 1};
int rownnz[] = {1, 2, 2};
int rowadr[] = {0, 1, 3};
mjtNum mat[] = {1, 2, 3, 4, 5};
mjtNum matT[5];
int colindT[5];
int rownnzT[2];
int rowadrT[2];
mju_transposeSparse(matT, mat, 3, 2, rownnzT, rowadrT, colindT, nullptr,
rownnz, rowadr, colind);
// compute rowsuperT
int rowsuperT[2];
mju_superSparse(2, rowsuperT, rownnzT, rowadrT, colindT);
// use old function as ground truth
int rownnzH_expected[] = {0, 0};
int rowadrH_expected[] = {0, 0};
int nnz_expected = mju_sqrMatTDSparseCount(
rownnzH_expected, rowadrH_expected, 2, rownnz, rowadr, colind, rownnzT,
rowadrT, colindT, rowsuperT, data, /*flg_upper=*/0);
// test new function with supernodes
int rownnzH[] = {0, 0};
int rowadrH[] = {0, 0};
int nnz = mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, nullptr, nullptr,
3, 2, rownnz, rowadr, colind, rownnzT,
rowadrT, colindT, rowsuperT, data);
EXPECT_EQ(nnz, nnz_expected);
EXPECT_THAT(rownnzH, ElementsAre(rownnzH_expected[0], rownnzH_expected[1]));
EXPECT_THAT(rowadrH, ElementsAre(rowadrH_expected[0], rowadrH_expected[1]));
// test fill mode with supernodes
std::vector<int> colindH(nnz, -1);
mju_sqrMatTDSparseSymbolic(rownnzH, rowadrH, colindH.data(), nullptr, 3, 2,
rownnz, rowadr, colind, rownnzT, rowadrT,
colindT, rowsuperT, data);
// verify all filled
for (int i = 0; i < nnz; i++) {
EXPECT_GE(colindH[i], 0) << "colindH[" << i << "] not filled";
}
// verify numeric phase with supernodes
std::vector<mjtNum> resH(nnz);
mjtNum diag[] = {1, 1, 1, 1, 1}; // dummy diagonal
mju_sqrMatTDSparseNumeric(resH.data(), 2, rownnzH, rowadrH, colindH.data(),
nullptr, mat, rownnz, rowadr, colind, matT, rownnzT,
rowadrT, colindT, rowsuperT, diag, data);
// ground truth numeric
std::vector<mjtNum> res_expected(4);
std::vector<int> colindH_expected(4);
int rownnzH_exp[] = {0, 0};
int rowadrH_exp[] = {0, 2};
mju_sqrMatTDSparse(res_expected.data(), mat, matT, diag, 3, 2, rownnzH_exp,
rowadrH_exp, colindH_expected.data(), rownnz, rowadr,
colind, nullptr, rownnzT, rowadrT, colindT, rowsuperT,
data, nullptr);
// compare values (sparse result vs sparse ground truth)
for (int r = 0; r < 2; r++) {
for (int i = 0; i < rownnzH[r]; i++) {
// find matching col in ground truth
int c = colindH[rowadrH[r] + i];
mjtNum val = resH[rowadrH[r] + i];
bool found = false;
for (int j = 0; j < rownnzH_exp[r]; j++) {
if (colindH_expected[rowadrH_exp[r] + j] == c) {
EXPECT_NEAR(val, res_expected[rowadrH_exp[r] + j], 1e-14);
found = true;
break;
}
}
EXPECT_TRUE(found) << "Column " << c
<< " not found in ground truth for row " << r;
}
}
mj_deleteData(data);
mj_deleteModel(model);
}
TEST_F(EngineUtilSparseTest, MjuSqrMatTDSparseNumeric) {
// Test numeric phase using symbolic phase + existing function as ground truth
// 1 2
// M = 3 4
mjModel* model = LoadModelFromString("<mujoco/>");
mjData* data = mj_makeData(model);
int colind[] = {0, 1, 0, 1};
int rownnz[] = {2, 2};
int rowadr[] = {0, 2};
mjtNum mat[] = {1, 2, 3, 4};
// compute transpose
mjtNum matT[4];
int colindT[4];
int rownnzT[2];
int rowadrT[2];
mju_transposeSparse(matT, mat, 2, 2, rownnzT, rowadrT, colindT, nullptr,
rownnz, rowadr, colind);
// compute supernodes
int rowsuperT[2];
mju_superSparse(2, rowsuperT, rownnzT, rowadrT, colindT);
mjtNum diag[] = {2, 3}; // diagonal weighting matrix
// test both diagind cases: lower-only (diagind=NULL) and both triangles
// (diagind!=NULL)
for (int use_diagind = 0; use_diagind <= 1; use_diagind++) {
// compute sparsity pattern using symbolic phase
int rownnzH[] = {0, 0};
int rowadrH[] = {0, 0};
int diagindH[] = {0, 0};
int nnz = mju_sqrMatTDSparseSymbolic(
rownnzH, rowadrH, nullptr, use_diagind ? diagindH : nullptr, 2, 2,
rownnz, rowadr, colind, rownnzT, rowadrT, colindT, nullptr, data);
std::vector<int> colindH(nnz);
mju_sqrMatTDSparseSymbolic(
rownnzH, rowadrH, colindH.data(), use_diagind ? diagindH : nullptr, 2,
2, rownnz, rowadr, colind, rownnzT, rowadrT, colindT, nullptr, data);
// compute values using numeric phase
std::vector<mjtNum> resH(nnz);
mju_sqrMatTDSparseNumeric(resH.data(), 2, rownnzH, rowadrH,
colindH.data(), use_diagind ? diagindH : nullptr,
mat, rownnz, rowadr, colind, matT, rownnzT,
rowadrT, colindT, rowsuperT, diag, data);
// compute ground truth using existing mju_sqrMatTDSparse
// use uncompressed storage to give the old function enough room
std::vector<mjtNum> res_expected(4); // 2x2 uncompressed
std::vector<int> colindH_expected(4);
int rownnzH_exp[] = {0, 0};
int rowadrH_exp[] = {0, 2};
int diagind_exp[] = {0, 0};
mju_sqrMatTDSparse(res_expected.data(), mat, matT, diag, 2, 2, rownnzH_exp,
rowadrH_exp, colindH_expected.data(), rownnz, rowadr,
colind, nullptr, rownnzT, rowadrT, colindT, nullptr,
data, use_diagind ? diagind_exp : nullptr);
// check that rownnz matches (nnz may differ due to compressed vs
// uncompressed storage)
EXPECT_EQ(rownnzH[0], rownnzH_exp[0])
<< "rownnz[0] mismatch for use_diagind=" << use_diagind;
EXPECT_EQ(rownnzH[1], rownnzH_exp[1])
<< "rownnz[1] mismatch for use_diagind=" << use_diagind;
// compare column indices and values for each row
for (int r = 0; r < 2; r++) {
for (int j = 0; j < rownnzH[r]; j++) {
int idx = rowadrH[r] + j;
int idx_exp = rowadrH_exp[r] + j;
EXPECT_EQ(colindH[idx], colindH_expected[idx_exp])
<< "colind mismatch at row " << r << " pos " << j
<< " for use_diagind=" << use_diagind;
EXPECT_NEAR(resH[idx], res_expected[idx_exp], 1e-10)
<< "value mismatch at row " << r << " pos " << j
<< " for use_diagind=" << use_diagind;
}
}
}
mj_deleteData(data);
mj_deleteModel(model);