1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
|
// 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 <cstdio>
#include <cstring>
#include <mujoco/mujoco.h>
// enable compilation with and without OpenMP support
#if defined(_OPENMP)
#include <omp.h>
#else
// omp timer replacement
#include <chrono>
double omp_get_wtime(void) {
static std::chrono::system_clock::time_point _start = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed = std::chrono::system_clock::now() - _start;
return elapsed.count();
}
// omp functions used below
void omp_set_dynamic(int) {}
void omp_set_num_threads(int) {}
int omp_get_num_procs(void) {
return 1;
}
#endif
// gloval variables: internal
const int MAXTHREAD = 64; // maximum number of threads allowed
const int MAXEPOCH = 100; // maximum number of epochs
int isforward = 0; // dynamics mode: forward or inverse
mjtNum* deriv = 0; // dynamics derivatives (6*nv*nv):
// dinv/dpos, dinv/dvel, dinv/dacc, dacc/dpos, dacc/dvel, dacc/dfrc
// global variables: user-defined, with defaults
int nthread = 0; // number of parallel threads (default set later)
int niter = 30; // fixed number of solver iterations for finite-differencing
int nwarmup = 3; // center point repetitions to improve warmstart
int nepoch = 20; // number of timing epochs
int nstep = 500; // number of simulation steps per epoch
double eps = 1e-6; // finite-difference epsilon
// worker function for parallel finite-difference computation of derivatives
void worker(const mjModel* m, const mjData* dmain, mjData* d, int id) {
int nv = m->nv;
// allocate stack space for result at center
mjMARKSTACK;
mjtNum* center = mj_stackAlloc(d, nv);
mjtNum* warmstart = mj_stackAlloc(d, nv);
// prepare static schedule: range of derivative columns to be computed by this thread
int chunk = (m->nv + nthread-1) / nthread;
int istart = id * chunk;
int iend = mjMIN(istart + chunk, m->nv);
// copy state and control from dmain to thread-specific d
d->time = dmain->time;
mju_copy(d->qpos, dmain->qpos, m->nq);
mju_copy(d->qvel, dmain->qvel, m->nv);
mju_copy(d->qacc, dmain->qacc, m->nv);
mju_copy(d->qacc_warmstart, dmain->qacc_warmstart, m->nv);
mju_copy(d->qfrc_applied, dmain->qfrc_applied, m->nv);
mju_copy(d->xfrc_applied, dmain->xfrc_applied, 6*m->nbody);
mju_copy(d->ctrl, dmain->ctrl, m->nu);
// run full computation at center point (usually faster than copying dmain)
if (isforward) {
mj_forward(m, d);
// extra solver iterations to improve warmstart (qacc) at center point
for (int rep=1; rep<nwarmup; rep++) {
mj_forwardSkip(m, d, mjSTAGE_VEL, 1);
}
} else {
mj_inverse(m, d);
}
// select output from forward or inverse dynamics
mjtNum* output = (isforward ? d->qacc : d->qfrc_inverse);
// save output for center point and warmstart (needed in forward only)
mju_copy(center, output, nv);
mju_copy(warmstart, d->qacc_warmstart, nv);
// select target vector and original vector for force or acceleration derivative
mjtNum* target = (isforward ? d->qfrc_applied : d->qacc);
const mjtNum* original = (isforward ? dmain->qfrc_applied : dmain->qacc);
// finite-difference over force or acceleration: skip = mjSTAGE_VEL
for (int i=istart; i<iend; i++) {
// perturb selected target
target[i] += eps;
// evaluate dynamics, with center warmstart
if (isforward) {
mju_copy(d->qacc_warmstart, warmstart, m->nv);
mj_forwardSkip(m, d, mjSTAGE_VEL, 1);
} else {
mj_inverseSkip(m, d, mjSTAGE_VEL, 1);
}
// undo perturbation
target[i] = original[i];
// compute column i of derivative 2
for (int j=0; j<nv; j++) {
deriv[(3*isforward+2)*nv*nv + i + j*nv] = (output[j] - center[j])/eps;
}
}
// finite-difference over velocity: skip = mjSTAGE_POS
for (int i=istart; i<iend; i++) {
// perturb velocity
d->qvel[i] += eps;
// evaluate dynamics, with center warmstart
if (isforward) {
mju_copy(d->qacc_warmstart, warmstart, m->nv);
mj_forwardSkip(m, d, mjSTAGE_POS, 1);
} else {
mj_inverseSkip(m, d, mjSTAGE_POS, 1);
}
// undo perturbation
d->qvel[i] = dmain->qvel[i];
// compute column i of derivative 1
for (int j=0; j<nv; j++) {
deriv[(3*isforward+1)*nv*nv + i + j*nv] = (output[j] - center[j])/eps;
}
}
// finite-difference over position: skip = mjSTAGE_NONE
for (int i=istart; i<iend; i++) {
// get joint id for this dof
int jid = m->dof_jntid[i];
// get quaternion address and dof position within quaternion (-1: not in quaternion)
int quatadr = -1, dofpos = 0;
if (m->jnt_type[jid]==mjJNT_BALL) {
quatadr = m->jnt_qposadr[jid];
dofpos = i - m->jnt_dofadr[jid];
} else if (m->jnt_type[jid]==mjJNT_FREE && i>=m->jnt_dofadr[jid]+3) {
quatadr = m->jnt_qposadr[jid] + 3;
dofpos = i - m->jnt_dofadr[jid] - 3;
}
// apply quaternion or simple perturbation
if (quatadr>=0) {
mjtNum angvel[3] = {0, 0, 0};
angvel[dofpos] = eps;
mju_quatIntegrate(d->qpos+quatadr, angvel, 1);
} else {
d->qpos[m->jnt_qposadr[jid] + i - m->jnt_dofadr[jid]] += eps;
}
// evaluate dynamics, with center warmstart
if (isforward) {
mju_copy(d->qacc_warmstart, warmstart, m->nv);
mj_forwardSkip(m, d, mjSTAGE_NONE, 1);
} else {
mj_inverseSkip(m, d, mjSTAGE_NONE, 1);
}
// undo perturbation
mju_copy(d->qpos, dmain->qpos, m->nq);
// compute column i of derivative 0
for (int j=0; j<nv; j++) {
deriv[(3*isforward+0)*nv*nv + i + j*nv] = (output[j] - center[j])/eps;
}
}
mjFREESTACK;
}
// compute relative L1 norm of residual
double relnorm(mjtNum* residual, mjtNum* base, int n) {
mjtNum L1res = 0, L1base = 0;
for (int i=0; i<n; i++) {
L1res += mju_abs(residual[i]);
L1base += mju_abs(base[i]);
}
return (double) mju_log10(mju_max(mjMINVAL, L1res/mju_max(mjMINVAL, L1base)));
}
// names of residuals for accuracy check
const char* accuracy[8] = {
"G2*F2 - I ",
"G2 - G2' ",
"G1 - G1' ",
"F2 - F2' ",
"G1 + G2*F1",
"G0 + G2*F0",
"F1 + F2*G1",
"F0 + F2*G0"
};
// check accuracy of derivatives using known mathematical identities
void checkderiv(const mjModel* m, mjData* d, mjtNum error[7]) {
int nv = m->nv;
// allocate space
mjMARKSTACK;
mjtNum* mat = mj_stackAlloc(d, nv*nv);
// get pointers to derivative matrices
mjtNum* G0 = deriv; // dinv/dpos
mjtNum* G1 = deriv + nv*nv; // dinv/dvel
mjtNum* G2 = deriv + 2*nv*nv; // dinv/dacc
mjtNum* F0 = deriv + 3*nv*nv; // dacc/dpos
mjtNum* F1 = deriv + 4*nv*nv; // dacc/dvel
mjtNum* F2 = deriv + 5*nv*nv; // dacc/dfrc
// G2*F2 - I
mju_mulMatMat(mat, G2, F2, nv, nv, nv);
for (int i=0; i<nv; i++) {
mat[i*(nv+1)] -= 1;
}
error[0] = relnorm(mat, G2, nv*nv);
// G2 - G2'
mju_transpose(mat, G2, nv, nv);
mju_sub(mat, mat, G2, nv*nv);
error[1] = relnorm(mat, G2, nv*nv);
// G1 - G1'
mju_transpose(mat, G1, nv, nv);
mju_sub(mat, mat, G1, nv*nv);
error[2] = relnorm(mat, G1, nv*nv);
// F2 - F2'
mju_transpose(mat, F2, nv, nv);
mju_sub(mat, mat, F2, nv*nv);
error[3] = relnorm(mat, F2, nv*nv);
// G1 + G2*F1
mju_mulMatMat(mat, G2, F1, nv, nv, nv);
mju_addTo(mat, G1, nv*nv);
error[4] = relnorm(mat, G1, nv*nv);
// G0 + G2*F0
mju_mulMatMat(mat, G2, F0, nv, nv, nv);
mju_addTo(mat, G0, nv*nv);
error[5] = relnorm(mat, G0, nv*nv);
// F1 + F2*G1
mju_mulMatMat(mat, F2, G1, nv, nv, nv);
mju_addTo(mat, F1, nv*nv);
error[6] = relnorm(mat, F1, nv*nv);
// F0 + F2*G0
mju_mulMatMat(mat, F2, G0, nv, nv, nv);
mju_addTo(mat, F0, nv*nv);
error[7] = relnorm(mat, F0, nv*nv);
mjFREESTACK;
}
// main function
int main(int argc, char** argv) {
// print help if not enough arguments
if (argc<2) {
std::printf("\n Arguments: modelfile [nthread niter nwarmup nepoch nstep eps]\n\n");
return 1;
}
// default nthread = number of logical cores (usually optimal)
nthread = omp_get_num_procs();
// get numeric command-line arguments
if (argc>2) {
std::sscanf(argv[2], "%d", &nthread);
}
if (argc>3) {
std::sscanf(argv[3], "%d", &niter);
}
if (argc>4) {
std::sscanf(argv[4], "%d", &nwarmup);
}
if (argc>5) {
std::sscanf(argv[5], "%d", &nepoch);
}
if (argc>6) {
std::sscanf(argv[6], "%d", &nstep);
}
if (argc>7) {
std::sscanf(argv[7], "%lf", &eps);
}
// check number of threads
if (nthread<1 || nthread>MAXTHREAD) {
std::printf("nthread must be between 1 and %d\n", MAXTHREAD);
return 1;
}
// check number of epochs
if (nepoch<1 || nepoch>MAXEPOCH) {
std::printf("nepoch must be between 1 and %d\n", MAXEPOCH);
return 1;
}
// load model
mjModel* m = 0;
if (std::strlen(argv[1])>4 && !std::strcmp(argv[1]+std::strlen(argv[1])-4, ".mjb")) {
m = mj_loadModel(argv[1], NULL);
} else {
m = mj_loadXML(argv[1], NULL, NULL, 0);
}
if (!m) {
std::printf("Could not load modelfile '%s'\n", argv[1]);
return 1;
}
// print arguments
#if defined(_OPENMP)
std::printf("\nnthread : %d (OpenMP)\n", nthread);
#else
std::printf("\nnthread : %d (serial)\n", nthread);
#endif
std::printf("niter : %d\n", niter);
std::printf("nwarmup : %d\n", nwarmup);
std::printf("nepoch : %d\n", nepoch);
std::printf("nstep : %d\n", nstep);
std::printf("eps : %g\n\n", eps);
// make mjData: main, per-thread
mjData* dmain = mj_makeData(m);
mjData* d[MAXTHREAD];
for (int n=0; n<nthread; n++) {
d[n] = mj_makeData(m);
}
// allocate derivatives
deriv = (mjtNum*) mju_malloc(6*sizeof(mjtNum)*m->nv*m->nv);
// set up OpenMP (if not enabled, this does nothing)
omp_set_dynamic(0);
omp_set_num_threads(nthread);
// save solver options
int save_iterations = m->opt.iterations;
mjtNum save_tolerance = m->opt.tolerance;
// allocate statistics
int nefc = 0;
double cputm[MAXEPOCH][2];
mjtNum error[MAXEPOCH][8];
// run epochs, collect statistics
for (int epoch=0; epoch<nepoch; epoch++) {
// set solver options for main simulation
m->opt.iterations = save_iterations;
m->opt.tolerance = save_tolerance;
// advance main simulation for nstep
for (int i=0; i<nstep; i++) {
mj_step(m, dmain);
}
// count number of active constraints
nefc += dmain->nefc;
// set solver options for finite differences
m->opt.iterations = niter;
m->opt.tolerance = 0;
// test forward and inverse
for (isforward=0; isforward<2; isforward++) {
// start timer
double starttm = omp_get_wtime();
// run worker threads in parallel if OpenMP is enabled
#pragma omp parallel for schedule(static)
for (int n=0; n<nthread; n++) {
worker(m, dmain, d[n], n);
}
// record duration in ms
cputm[epoch][isforward] = 1000*(omp_get_wtime() - starttm);
}
// check derivatives
checkderiv(m, d[0], error[epoch]);
}
// compute statistics
double mcputm[2] = {0, 0}, merror[8] = {0, 0, 0, 0, 0, 0, 0, 0};
for (int epoch=0; epoch<nepoch; epoch++) {
mcputm[0] += cputm[epoch][0];
mcputm[1] += cputm[epoch][1];
for (int ie=0; ie<8; ie++) {
merror[ie] += error[epoch][ie];
}
}
// print sizes, timing, accuracy
std::printf("sizes : nv %d, nefc %d\n\n", m->nv, nefc/nepoch);
std::printf("inverse : %.2f ms\n", mcputm[0]/nepoch);
std::printf("forward : %.2f ms\n\n", mcputm[1]/nepoch);
std::printf("accuracy: log10(residual L1 relnorm)\n");
std::printf("------------------------------------\n");
for (int ie=0; ie<8; ie++) {
std::printf(" %s : %.2g\n", accuracy[ie], merror[ie]/nepoch);
}
std::printf("\n");
// shut down
mju_free(deriv);
mj_deleteData(dmain);
for (int n=0; n<nthread; n++) {
mj_deleteData(d[n]);
}
mj_deleteModel(m);
return 0;
}
|