// 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_solve.h" #include #include #include #include #include "engine/engine_io.h" #include "engine/engine_macro.h" #include "engine/engine_util_blas.h" #include "engine/engine_util_errmem.h" #include "engine/engine_util_misc.h" #include "engine/engine_util_sparse.h" #include "engine/engine_util_spatial.h" //---------------------------- dense Cholesky ------------------------------------------------------ // Cholesky decomposition: mat = L*L'; return 'rank' int mju_cholFactor(mjtNum* mat, int n, mjtNum mindiag) { int rank = n; mjtNum tmp; // in-place Cholesky factorization for (int j=0; j=0; i--) { if (i0 && colind[rowadr[r]+rownnz[r]-1]>r) { rownnz[r]--; } // check if (rownnz[r]==0 || colind[rowadr[r]+rownnz[r]-1]!=r) { mju_error("Matrix must have non-zero diagonal in mju_cholFactorSparse"); } } // backpass over rows for (int r=n-1; r>=0; r--) { // get rownnz and rowadr for row r int nnz = rownnz[r], adr = rowadr[r]; // update row r diagonal mjtNum tmp = mat[adr+nnz-1]; if (tmp=0; i--) { if (res[i]) { // get rowadr[i], rownnz[i] const int adr = rowadr[i], nnz = rownnz[i]; // x(i) /= L(i,i) res[i] /= mat[adr+nnz-1]; mjtNum tmp = res[i]; // x(j) -= L(i,j)*x(i), j=0:i-1 for (int j=0; j1) { res[i] -= mju_dotSparse(mat+adr, res, nnz-1, colind+adr); // modulo AVX, the above line does // for (int j=0; j=0) { // get rownnz and rowadr for this row int nnz = rownnz[x_ind[i]], adr = rowadr[x_ind[i]]; // compute quantities mjtNum tmp = mat[adr+nnz-1]*mat[adr+nnz-1] + (flg_plus ? x[i]*x[i] : -x[i]*x[i]); if (tmp=0; i--) { // get address of last remaining element of row i, adjust remaining counter int ii = rowadr[i] + remaining[i] - 1; remaining[i]--; // make sure ii is on diagonal if (colind[ii]!=i) { mju_error("missing diagonal element in mju_factorLUSparse"); } // make sure diagonal is not too small if (mju_abs(LU[ii])=0; j--) { // get address of last remaining element of row j int ji = rowadr[j] + remaining[j] - 1; // process row j if (j,i) is non-zero if (colind[ji]==i) { // adjust remaining counter remaining[j]--; // (j,i) = (j,i) / (i,i) LU[ji] = LU[ji] / LU[ii]; mjtNum LUji = LU[ji]; // (j,k) = (j,k) - (i,k) * (j,i) for kcolind[jcnt]) { // advance j counter jcnt++; } // only (i,k) non-zero else { mju_error("mju_factorLUSparse requires fill-in"); } } // make sure both rows fully processed if (icnt!=rowadr[i]+remaining[i] || jcnt!=rowadr[j]+remaining[j]) { mju_error("row processing incomplete in mju_factorLUSparse"); } } } } // make sure remaining points to diagonal for (int i=0; i=0; i--) { // init: diagonal of (U+I) is 1 res[i] = vec[i]; // res[i] -= sum_k>i res[k]*LU(i,k) int j = rownnz[i] - 1; while (colind[rowadr[i]+j]>i) { res[i] -= res[colind[rowadr[i]+j]] * LU[rowadr[i]+j]; j--; } // make sure j points to diagonal if (colind[rowadr[i]+j]!=i) { mju_error("diagonal of U not reached in mju_factorLUSparse"); } } //------------------ solve L*res(new) = res for (int i=0; ifabs(D[2]) && fabs(D[1])>fabs(D[5])) { rk = 0; // row ck = 1; // column rotk = 2; // rotation axis } else if (fabs(D[2])>fabs(D[5])) { rk = 0; ck = 2; rotk = 1; } else { rk = 1; ck = 2; rotk = 0; } // terminate if max off-diagonal element too small if (fabs(D[3*rk+ck])=0) { t = 1.0/(tau + mju_sqrt(1 + tau*tau)); } else { t = -1.0/(-tau + mju_sqrt(1 + tau*tau)); } c = 1.0/mju_sqrt(1 + t*t); // terminate if cosine too close to 1 if (c>1.0-eigEPS) { break; } // express rotation as quaternion tmp[1] = tmp[2] = tmp[3] = 0; tmp[rotk+1] = (tau>=0 ? -mju_sqrt(0.5-0.5*c) : mju_sqrt(0.5-0.5*c)); if (rotk==1) { tmp[rotk+1] = -tmp[rotk+1]; } tmp[0] = mju_sqrt(1.0 - tmp[rotk+1]*tmp[rotk+1]); mju_normalize4(tmp); // accumulate quaternion rotation mju_mulQuat(quat, quat, tmp); mju_normalize4(quat); } // sort eigenvalues in decreasing order (bubblesort: 0, 1, 0) for (int j=0; j<3; j++) { int j1 = j%2; // lead index if (eigval[j1] < eigval[j1+1]) { // swap eigenvalues t = eigval[j1]; eigval[j1] = eigval[j1+1]; eigval[j1+1] = t; // rotate quaternion tmp[0] = 0.707106781186548; // mju_cos(pi/4) = mju_sin(pi/4) tmp[1] = tmp[2] = tmp[3] = 0; tmp[(j1+2)%3+1] = tmp[0]; mju_mulQuat(quat, quat, tmp); mju_normalize4(quat); } } // recompute eigvec mju_quat2Mat(eigvec, quat); return iter; } //---------------------------------- QCQP ---------------------------------------------------------- // solve QCQP in 2 dimensions: // min 0.5*x'*A*x + x'*b s.t. sum (xi/di)^2 <= r^2 // return 0 if unconstrained, 1 if constrained int mju_QCQP2(mjtNum* res, const mjtNum* Ain, const mjtNum* bin, const mjtNum* d, mjtNum r) { mjtNum A11, A22, A12, b1, b2; mjtNum P11, P22, P12, det, detinv, v1, v2, la, val, deriv; // scale A,b so that constraint becomes x'*x <= r*r b1 = bin[0]*d[0]; b2 = bin[1]*d[1]; A11 = Ain[0]*d[0]*d[0]; A22 = Ain[3]*d[1]*d[1]; A12 = Ain[1]*d[0]*d[1]; // Newton iteration la = 0; for (int iter=0; iter<20; iter++) { // det(A+la) det = (A11+la)*(A22+la) - A12*A12; // check SPD, with 1e-10 threshold if (det<1e-10) { res[0] = 0; res[1] = 0; return 0; } // P = inv(A+la) detinv = 1/det; P11 = (A22+la)*detinv; P22 = (A11+la)*detinv; P12 = -A12*detinv; // v = -P*b v1 = -P11*b1 - P12*b2; v2 = -P12*b1 - P22*b2; // val = v'*v - r*r val = v1*v1 + v2*v2 - r*r; // check for convergence, or initial solution inside constraint set if (val<1e-10) { break; } // deriv = -2 * v' * P * v deriv = -2.0*(P11*v1*v1 + 2.0*P12*v1*v2 + P22*v2*v2); // compute update, exit if too small mjtNum delta = -val/deriv; if (delta<1e-10) { break; } // update la += delta; } // undo scaling res[0] = v1*d[0]; res[1] = v2*d[1]; return (la!=0); } // solve QCQP in 3 dimensions: // min 0.5*x'*A*x + x'*b s.t. sum (xi/di)^2 <= r^2 // return 0 if unconstrained, 1 if constrained int mju_QCQP3(mjtNum* res, const mjtNum* Ain, const mjtNum* bin, const mjtNum* d, mjtNum r) { mjtNum A11, A22, A33, A12, A13, A23, b1, b2, b3; mjtNum P11, P22, P33, P12, P13, P23, det, detinv, v1, v2, v3, la, val, deriv; // scale A,b so that constraint becomes x'*x <= r*r b1 = bin[0]*d[0]; b2 = bin[1]*d[1]; b3 = bin[2]*d[2]; A11 = Ain[0]*d[0]*d[0]; A22 = Ain[4]*d[1]*d[1]; A33 = Ain[8]*d[2]*d[2]; A12 = Ain[1]*d[0]*d[1]; A13 = Ain[2]*d[0]*d[2]; A23 = Ain[5]*d[1]*d[2]; // Newton iteration la = 0; for (int iter=0; iter<20; iter++) { // unscaled P P11 = (A22+la)*(A33+la) - A23*A23; P22 = (A11+la)*(A33+la) - A13*A13; P33 = (A11+la)*(A22+la) - A12*A12; P12 = A13*A23 - A12*(A33+la); P13 = A12*A23 - A13*(A22+la); P23 = A12*A13 - A23*(A11+la); // det(A+la) det = (A11+la)*P11 + A12*P12 + A13*P13; // check SPD, with 1e-10 threshold if (det<1e-10) { res[0] = 0; res[1] = 0; res[2] = 0; return 0; } // detinv detinv = 1/det; // final P P11 *= detinv; P22 *= detinv; P33 *= detinv; P12 *= detinv; P13 *= detinv; P23 *= detinv; // v = -P*b v1 = -P11*b1 - P12*b2 - P13*b3; v2 = -P12*b1 - P22*b2 - P23*b3; v3 = -P13*b1 - P23*b2 - P33*b3; // val = v'*v - r*r val = v1*v1 + v2*v2 + v3*v3 - r*r; // check for convergence, or initial solution inside constraint set if (val<1e-10) { break; } // deriv = -2 * v' * P * v deriv = -2.0*(P11*v1*v1 + P22*v2*v2 + P33*v3*v3) -4.0*(P12*v1*v2 + P13*v1*v3 + P23*v2*v3); // compute update, exit if too small mjtNum delta = -val/deriv; if (delta<1e-10) { break; } // update la += delta; } // undo scaling res[0] = v1*d[0]; res[1] = v2*d[1]; res[2] = v3*d[2]; return (la!=0); } // solve QCQP in n dimensions: // min 0.5*x'*A*x + x'*b s.t. sum (xi/di)^2 <= r^2 // return 0 if unconstrained, 1 if constrained int mju_QCQP(mjtNum* res, const mjtNum* Ain, const mjtNum* bin, const mjtNum* d, mjtNum r, int n) { mjtNum A[25], Ala[25], b[5]; mjtNum la, val, deriv, tmp[5]; // check size if (n>5) { mju_error("mju_QCQP supports n up to 5"); } // scale A,b so that constraint becomes x'*x <= r*r for (int i=0; i= upper[i]) { mju_error("mju_boxQP: upper bounds must be stricly larger than lower bounds"); } } } // local scratch vectors, allocate in R mjtNum* scratch = R + n*n; mjtNum* grad = scratch + 0*n; mjtNum* search = scratch + 1*n; mjtNum* candidate = scratch + 2*n; mjtNum* temp = scratch + 3*n; int* clamped = (int*) (scratch + 4*n); int* oldclamped = (int*) (scratch + 5*n); // if index vector not given, use scratch space if (!index) { index = (int*) (scratch + 6*n); } static const char status_string[mjNBOXQP][50]= { "Hessian is not positive definite", "No descent direction found", "Maximum main iterations exceeded", "Maximum line-search iterations exceeded", "Gradient norm smaller than tolerance", "No dimensions clamped, returning Newton point", "All dimensions clamped" }; // no bounds: return Newton point if (!lower && !upper) { // try to factorize mju_copy(R, H, n*n); int rank = mju_cholFactor(R, n, mjMINVAL); if (rank == n) { mju_cholSolve(res, R, g, n); mju_scl(res, res, -1, n); nfactor = 1; status = mjBOXQP_UNBOUNDED; } else { status = mjBOXQP_NOT_SPD; } // full index set (no clamping) for (int i=0; i 0 ) || ( upper && res[i] == upper[i] && grad[i] < 0 ); } // build index of free dimensions, count them nfree = 0; for (int i=0; i= 0) { break; // SHOULD NOT OCCUR } // ------ projected Armijo line search mjtNum step = 1; int nstep = 0; do { // candidate = clamp(x + step*search) mju_scl(candidate, search, step, n); mju_addTo(candidate, res, n); for (int i=0; iupper[i]) { candidate[i] = upper[i]; } } // new objective value value = 0.5 * mulVecMatVecSym(candidate, H, n) + mju_dot(candidate, g, n); // increment and break if step is too small nstep++; step = step*backtrack; if (step= Armijo improvement = (value - oldvalue) / (step*sdotg); } while (improvement < armijo); // print iteration info if (log) { logptr += snprintf(log+logptr, logsz-logptr, "iter %-3d: |grad|: %-8.2g reduction: %-8.2g improvement: %-8.4g " "linesearch: %g^%-2d factorized: %d nfree: %d\n", iter+1, mju_sqrt(norm2), oldvalue-value, improvement, backtrack, nstep-1, factorize, nfree); } // accept candidate mju_copy(res, candidate, n); } // max iterations exceeded if (iter==maxiter) { status = mjBOXQP_MAX_ITER; } // print final info if (log) { snprintf(log+logptr, logsz-logptr, "BOXQP: %s.\n" "iterations= %d, factorizations= %d, |grad|= %-12.6g, final value= %-12.6g\n", status_string[status+1], iter, nfactor, mju_sqrt(norm2), value); } // return nf or -1 if failure return (status == mjBOXQP_NO_DESCENT || status == mjBOXQP_NOT_SPD) ? -1 : nfree; }