3dec35a91e
PiperOrigin-RevId: 797826112 Change-Id: Ibc51624adc3ec42c265958bf77696b33231431c1
611 lines
22 KiB
Python
611 lines
22 KiB
Python
# Copyright 2023 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.
|
|
# ==============================================================================
|
|
"""Constraint solvers."""
|
|
|
|
import jax
|
|
from jax import numpy as jp
|
|
import mujoco
|
|
from mujoco.mjx._src import math
|
|
from mujoco.mjx._src import smooth
|
|
from mujoco.mjx._src import support
|
|
# pylint: disable=g-importing-member
|
|
from mujoco.mjx._src.dataclasses import PyTreeNode
|
|
from mujoco.mjx._src.types import ConeType
|
|
from mujoco.mjx._src.types import Data
|
|
from mujoco.mjx._src.types import DataJAX
|
|
from mujoco.mjx._src.types import DisableBit
|
|
from mujoco.mjx._src.types import Model
|
|
from mujoco.mjx._src.types import ModelJAX
|
|
from mujoco.mjx._src.types import OptionJAX
|
|
from mujoco.mjx._src.types import SolverType
|
|
# pylint: enable=g-importing-member
|
|
|
|
|
|
class Context(PyTreeNode):
|
|
"""Data updated during each solver iteration.
|
|
|
|
Attributes:
|
|
qacc: acceleration (from Data) (nv,)
|
|
qfrc_constraint: constraint force (from Data) (nv,)
|
|
Jaref: Jac*qacc - aref (nefc,)
|
|
efc_force: constraint force in constraint space (nefc,)
|
|
Ma: M*qacc (nv,)
|
|
grad: gradient of master cost (nv,)
|
|
Mgrad: M / grad (nv,)
|
|
search: linesearch vector (nv,)
|
|
gauss: gauss Cost
|
|
cost: constraint + Gauss cost
|
|
prev_cost: cost from previous iter
|
|
solver_niter: number of solver iterations
|
|
active: active (quadratic) constraints (nefc,)
|
|
fri: friction of regularized cone (num(con.dim > 1), 6)
|
|
dm: regularized constraint mass (num(con.dim > 1))
|
|
u: friction cone (normal and tangents) (num(con.dim > 1), 6)
|
|
h: cone hessian (num(con.dim > 1), 6, 6)
|
|
"""
|
|
|
|
qacc: jax.Array
|
|
qfrc_constraint: jax.Array
|
|
Jaref: jax.Array # pylint: disable=invalid-name
|
|
efc_force: jax.Array
|
|
Ma: jax.Array # pylint: disable=invalid-name
|
|
grad: jax.Array
|
|
Mgrad: jax.Array # pylint: disable=invalid-name
|
|
search: jax.Array
|
|
gauss: jax.Array
|
|
cost: jax.Array
|
|
prev_cost: jax.Array
|
|
solver_niter: jax.Array
|
|
active: jax.Array
|
|
fri: jax.Array
|
|
dm: jax.Array
|
|
u: jax.Array
|
|
h: jax.Array
|
|
|
|
@classmethod
|
|
def create(cls, m: Model, d: Data, grad: bool = True) -> 'Context':
|
|
if not isinstance(d._impl, DataJAX) or not isinstance(
|
|
m.opt._impl, OptionJAX
|
|
):
|
|
raise ValueError(
|
|
'Constraint context requires JAX backend implementation.'
|
|
)
|
|
|
|
jaref = d._impl.efc_J @ d.qacc - d._impl.efc_aref
|
|
# TODO(robotics-team): determine nv at which sparse mul is faster
|
|
ma = support.mul_m(m, d, d.qacc)
|
|
nv_0 = jp.zeros(m.nv)
|
|
fri = 0.0
|
|
if m.opt.cone == ConeType.ELLIPTIC:
|
|
friction = d._impl.contact.friction[d._impl.contact.dim > 1]
|
|
dim = d._impl.contact.dim[d._impl.contact.dim > 1]
|
|
mu = friction[:, 0] / jp.sqrt(m.opt.impratio)
|
|
fri = jp.concatenate((mu[:, None], friction), axis=1)
|
|
for condim in (3, 4, 6):
|
|
fri = fri.at[dim == condim, condim:].set(0)
|
|
|
|
ctx = Context(
|
|
qacc=d.qacc,
|
|
qfrc_constraint=d.qfrc_constraint,
|
|
Jaref=jaref,
|
|
efc_force=d._impl.efc_force,
|
|
Ma=ma,
|
|
grad=nv_0,
|
|
Mgrad=nv_0,
|
|
search=nv_0,
|
|
gauss=0.0,
|
|
cost=jp.inf,
|
|
prev_cost=0.0,
|
|
solver_niter=0,
|
|
active=0.0,
|
|
fri=fri,
|
|
dm=0.0,
|
|
u=0.0,
|
|
h=0.0,
|
|
)
|
|
ctx = _update_constraint(m, d, ctx)
|
|
if grad:
|
|
ctx = _update_gradient(m, d, ctx)
|
|
ctx = ctx.replace(search=-ctx.Mgrad) # start with preconditioned gradient
|
|
|
|
return ctx
|
|
|
|
|
|
class _LSPoint(PyTreeNode):
|
|
"""Line search evaluation point.
|
|
|
|
Attributes:
|
|
alpha: step size that reduces f(x + alpha * p) given search direction p
|
|
cost: line search cost
|
|
deriv_0: first derivative of quadratic
|
|
deriv_1: second derivative of quadratic
|
|
"""
|
|
|
|
alpha: jax.Array
|
|
cost: jax.Array
|
|
deriv_0: jax.Array
|
|
deriv_1: jax.Array
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
m: Model,
|
|
d: Data,
|
|
ctx: Context,
|
|
alpha: jax.Array,
|
|
jv: jax.Array,
|
|
quad: jax.Array,
|
|
quad_gauss: jax.Array,
|
|
uu: jax.Array,
|
|
v0: jax.Array,
|
|
uv: jax.Array,
|
|
vv: jax.Array,
|
|
) -> '_LSPoint':
|
|
"""Creates a linesearch point with first and second derivatives."""
|
|
# roughly corresponds to CGEval in mujoco/src/engine/engine_solver.c
|
|
if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX):
|
|
raise ValueError('LSPoint requires JAX backend implementation.')
|
|
|
|
cost, deriv_0, deriv_1 = 0.0, 0.0, 0.0
|
|
quad_total = quad_gauss
|
|
x = ctx.Jaref + alpha * jv
|
|
active = (x < 0).at[: d._impl.ne + d._impl.nf].set(True)
|
|
|
|
dof_fl, ten_fl = m._impl.dof_hasfrictionloss, m._impl.tendon_hasfrictionloss
|
|
if (dof_fl.any() or ten_fl.any()) and not (
|
|
m.opt.disableflags & DisableBit.FRICTIONLOSS
|
|
):
|
|
f = d._impl.efc_frictionloss
|
|
r = 1.0 / (d._impl.efc_D + (d._impl.efc_D == 0.0) * mujoco.mjMINVAL)
|
|
rf, z = r * f, jp.zeros_like(f)
|
|
linear_neg = (x <= -rf)[:, None]
|
|
linear_pos = (x >= rf)[:, None]
|
|
qf = linear_neg * jp.array([f * (-0.5 * rf - ctx.Jaref), -f * jv, z]).T
|
|
qf += linear_pos * jp.array([f * (-0.5 * rf + ctx.Jaref), f * jv, z]).T
|
|
quad = jp.where((linear_neg | linear_pos) & (f[:, None] > 0), qf, quad)
|
|
|
|
if m.opt.cone == ConeType.ELLIPTIC:
|
|
mu, u0 = ctx.fri[:, 0], ctx.u[:, 0]
|
|
n = u0 + alpha * v0
|
|
tsqr = uu + alpha * (2 * uv + alpha * vv)
|
|
t = jp.sqrt(tsqr) # tangential force
|
|
|
|
bottom_zone = ((tsqr <= 0) & (n < 0)) | ((tsqr > 0) & ((mu * n + t) <= 0))
|
|
middle_zone = (tsqr > 0) & (n < (mu * t)) & ((mu * n + t) > 0)
|
|
|
|
# quadratic cost for equality, friction, limits, frictionless contacts
|
|
dim1 = d._impl.contact.efc_address[d._impl.contact.dim == 1]
|
|
nefl = d._impl.ne + d._impl.nf + d._impl.nl
|
|
active = active.at[nefl:].set(False).at[dim1].set(active[dim1])
|
|
quad_efld = jax.vmap(jp.multiply)(quad, active)
|
|
quad_total += jp.sum(quad_efld, axis=0)
|
|
# elliptic bottom zone: quadratic cost
|
|
efc_elliptic = d._impl.contact.efc_address[d._impl.contact.dim > 1]
|
|
quad_c = jax.vmap(jp.multiply)(quad[efc_elliptic], bottom_zone)
|
|
quad_total += jp.sum(quad_c, axis=0)
|
|
# elliptic middle zone
|
|
t += (t == 0) * mujoco.mjMINVAL
|
|
tsqr += (tsqr == 0) * mujoco.mjMINVAL
|
|
n1 = v0
|
|
t1 = (uv + alpha * vv) / t
|
|
t2 = vv / t - (uv + alpha * vv) * t1 / tsqr
|
|
dm = ctx.dm * middle_zone
|
|
nmt = n - mu * t
|
|
cost = 0.5 * jp.sum(dm * jp.square(nmt))
|
|
deriv_0 = jp.sum(dm * nmt * (n1 - mu * t1))
|
|
deriv_1 = jp.sum(dm * (jp.square(n1 - mu * t1) - nmt * mu * t2))
|
|
elif m.opt.cone == ConeType.PYRAMIDAL:
|
|
quad = jax.vmap(jp.multiply)(quad, active) # only active
|
|
quad_total += jp.sum(quad, axis=0)
|
|
else:
|
|
raise NotImplementedError(f'unsupported cone type: {m.opt.cone}')
|
|
|
|
alpha_sq = alpha * alpha
|
|
cost += alpha_sq * quad_total[2] + alpha * quad_total[1] + quad_total[0]
|
|
deriv_0 += 2 * alpha * quad_total[2] + quad_total[1]
|
|
deriv_1 += 2 * quad_total[2] + (quad_total[2] == 0) * mujoco.mjMINVAL
|
|
|
|
return _LSPoint(alpha=alpha, cost=cost, deriv_0=deriv_0, deriv_1=deriv_1)
|
|
|
|
|
|
class _LSContext(PyTreeNode):
|
|
"""Data updated during each line search iteration.
|
|
|
|
Attributes:
|
|
lo: low point bounding the line search interval
|
|
hi: high point bounding the line search interval
|
|
swap: True if low or hi was swapped in the line search iteration
|
|
ls_iter: number of linesearch iterations
|
|
"""
|
|
|
|
lo: _LSPoint
|
|
hi: _LSPoint
|
|
swap: jax.Array
|
|
ls_iter: jax.Array
|
|
|
|
|
|
def _while_loop_scan(cond_fun, body_fun, init_val, max_iter):
|
|
"""Scan-based implementation (jit ok, reverse-mode autodiff ok)."""
|
|
|
|
def _iter(val):
|
|
next_val = body_fun(val)
|
|
next_cond = cond_fun(next_val)
|
|
return next_val, next_cond
|
|
|
|
def _fun(tup, it):
|
|
val, cond = tup
|
|
# When cond is met, we start doing no-ops.
|
|
return jax.lax.cond(cond, _iter, lambda x: (x, False), val), it
|
|
|
|
init = (init_val, cond_fun(init_val))
|
|
return jax.lax.scan(_fun, init, None, length=max_iter)[0][0]
|
|
|
|
|
|
def _update_constraint(m: Model, d: Data, ctx: Context) -> Context:
|
|
"""Updates constraint force and resulting cost given last solver iteration.
|
|
|
|
Corresponds to CGupdateConstraint in mujoco/src/engine/engine_solver.c
|
|
|
|
Args:
|
|
m: model defining constraints
|
|
d: data which contains latest qacc and smooth terms
|
|
ctx: current solver context
|
|
|
|
Returns:
|
|
context with new constraint force and costs
|
|
"""
|
|
if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX):
|
|
raise ValueError('_update_constraint requires JAX backend implementation.')
|
|
|
|
# ne constraints are always active, nf are conditionally active, others are
|
|
# non-negative constraints.
|
|
active = (ctx.Jaref < 0).at[: d._impl.ne + d._impl.nf].set(True)
|
|
|
|
floss_force, floss_cost = jp.zeros(d._impl.nefc), 0.0
|
|
dof_fl, ten_fl = m._impl.dof_hasfrictionloss, m._impl.tendon_hasfrictionloss
|
|
if (dof_fl.any() or ten_fl.any()) and not (
|
|
m.opt.disableflags & DisableBit.FRICTIONLOSS
|
|
):
|
|
f = d._impl.efc_frictionloss
|
|
r = 1.0 / (d._impl.efc_D + (d._impl.efc_D == 0.0) * mujoco.mjMINVAL)
|
|
linear_neg = (ctx.Jaref <= -r * f) * (f > 0)
|
|
linear_pos = (ctx.Jaref >= r * f) * (f > 0)
|
|
active = active & ~linear_neg & ~linear_pos
|
|
floss_force = linear_neg * f + linear_pos * -f
|
|
floss_cost = linear_neg * (-0.5 * r * f * f - f * ctx.Jaref)
|
|
floss_cost += linear_pos * (-0.5 * r * f * f + f * ctx.Jaref)
|
|
floss_cost = floss_cost.sum()
|
|
|
|
if m.opt.cone == ConeType.PYRAMIDAL:
|
|
efc_force = d._impl.efc_D * -ctx.Jaref * active + floss_force
|
|
cost = 0.5 * jp.sum(d._impl.efc_D * ctx.Jaref * ctx.Jaref * active)
|
|
dm, u, h = 0.0, 0.0, 0.0
|
|
elif m.opt.cone == ConeType.ELLIPTIC:
|
|
friction = d._impl.contact.friction[d._impl.contact.dim > 1]
|
|
efc_address = d._impl.contact.efc_address[d._impl.contact.dim > 1]
|
|
dim = d._impl.contact.dim[d._impl.contact.dim > 1]
|
|
# to prevent out of range append zeros to ctx.Jaref
|
|
slice_fn = jax.vmap(
|
|
lambda x: jax.lax.dynamic_slice(
|
|
jp.concatenate((ctx.Jaref, jp.zeros((3)))), (x,), (6,)
|
|
)
|
|
)
|
|
u = slice_fn(efc_address) * ctx.fri
|
|
mu, n, t = ctx.fri[:, 0], u[:, 0], jax.vmap(math.norm)(u[:, 1:])
|
|
|
|
# bottom zone: quadratic
|
|
bottom_zone = ((t <= 0) & (n < 0)) | ((t > 0) & ((mu * n + t) <= 0))
|
|
adr_i, adr_j = [], []
|
|
for i, (condim, addr) in enumerate(zip(dim, efc_address)):
|
|
adr_i.extend(range(addr, addr + condim))
|
|
adr_j.extend([i] * condim)
|
|
active = active.at[jp.array(adr_i)].set(bottom_zone[jp.array(adr_j)])
|
|
efc_force = d._impl.efc_D * -ctx.Jaref * active + floss_force
|
|
cost = 0.5 * jp.sum(d._impl.efc_D * ctx.Jaref * ctx.Jaref * active)
|
|
|
|
# middle zone: cone
|
|
middle_zone = (t > 0) & (n < (mu * t)) & ((mu * n + t) > 0)
|
|
dm = d._impl.efc_D[efc_address] / jp.maximum(
|
|
mu * mu * (1 + mu * mu), mujoco.mjMINVAL
|
|
)
|
|
nmt = n - mu * t
|
|
cost += 0.5 * jp.sum(dm * nmt * nmt * middle_zone)
|
|
# tangent and friction for middle zone:
|
|
force = -dm * nmt * mu * middle_zone
|
|
force_fri = -force / (t + ~middle_zone * mujoco.mjMINVAL)
|
|
force_fri = force_fri[:, None] * u[:, 1:] * friction
|
|
efc_force = efc_force.at[efc_address].add(force)
|
|
efc_adr, adr_i, adr_j = [], [], []
|
|
for i, (condim, addr) in enumerate(zip(dim, efc_address)):
|
|
efc_adr.extend(range(addr + 1, addr + condim))
|
|
adr_i.extend([i] * (condim - 1))
|
|
adr_j.extend(range(condim - 1))
|
|
efc_adr, adr_i, adr_j = jp.array(efc_adr), jp.array(adr_i), jp.array(adr_j)
|
|
efc_force = efc_force.at[efc_adr].add(force_fri[(adr_i, adr_j)])
|
|
|
|
# cone hessian
|
|
h = 0.0
|
|
if m.opt.solver == SolverType.NEWTON:
|
|
t = jp.maximum(t, mujoco.mjMINVAL)
|
|
# h = mu*N/T^3 * U*U'
|
|
ttt = jp.maximum(t * t * t, mujoco.mjMINVAL)
|
|
h = jax.vmap(lambda x, y: x * jp.outer(y, y.T))(mu * n / ttt, u)
|
|
# add to diagonal: (mu^2 - mu*N/T) * I
|
|
h += jax.vmap(lambda x: x * jp.eye(6, 6))(mu * mu - mu * n / t)
|
|
# set first row: (1, -mu/T * U)
|
|
h_0 = jax.vmap(lambda mu, t, u: jp.append(1, -mu / t * u[1:]))(mu, t, u)
|
|
h = h.at[:, 0].set(h_0).at[:, :, 0].set(h_0)
|
|
# pre and post multiply by diag(mu, friction), scale by Dm
|
|
h *= jax.vmap(lambda d, f: d * jp.outer(f, f.T))(dm, ctx.fri)
|
|
# only cone constraints
|
|
h = jax.vmap(jp.multiply)(h, middle_zone)
|
|
else:
|
|
raise NotImplementedError(f'unsupported cone type: {m.opt.cone}')
|
|
|
|
qfrc_constraint = d._impl.efc_J.T @ efc_force
|
|
gauss = 0.5 * jp.dot(ctx.Ma - d.qfrc_smooth, ctx.qacc - d.qacc_smooth)
|
|
ctx = ctx.replace(
|
|
qfrc_constraint=qfrc_constraint,
|
|
gauss=gauss,
|
|
cost=cost + gauss + floss_cost,
|
|
prev_cost=ctx.cost,
|
|
efc_force=efc_force,
|
|
active=active,
|
|
dm=dm,
|
|
u=u,
|
|
h=h,
|
|
)
|
|
|
|
return ctx
|
|
|
|
|
|
def _update_gradient(m: Model, d: Data, ctx: Context) -> Context:
|
|
"""Updates grad and M / grad given latest solver iteration.
|
|
|
|
Corresponds to CGupdateGradient in mujoco/src/engine/engine_solver.c
|
|
|
|
Args:
|
|
m: model defining constraints
|
|
d: data which contains latest smooth terms
|
|
ctx: current solver context
|
|
|
|
Returns:
|
|
context with new grad and M / grad
|
|
Raises:
|
|
NotImplementedError: for unsupported solver type
|
|
"""
|
|
if not isinstance(m._impl, ModelJAX) or not isinstance(d._impl, DataJAX):
|
|
raise ValueError('_update_gradient requires JAX backend implementation.')
|
|
|
|
grad = ctx.Ma - d.qfrc_smooth - ctx.qfrc_constraint
|
|
|
|
if m.opt.solver == SolverType.CG:
|
|
mgrad = smooth.solve_m(m, d, grad)
|
|
elif m.opt.solver == SolverType.NEWTON:
|
|
if m.opt.cone == ConeType.ELLIPTIC:
|
|
cm = jp.diag(d._impl.efc_D * ctx.active)
|
|
efc_address = d._impl.contact.efc_address[d._impl.contact.dim > 1]
|
|
dim = d._impl.contact.dim[d._impl.contact.dim > 1]
|
|
# set efc of cone H along diagonal
|
|
for i, (condim, addr) in enumerate(zip(dim, efc_address)):
|
|
h_cone = ctx.h[i, :condim, :condim]
|
|
cm = cm.at[addr : addr + condim, addr : addr + condim].add(h_cone)
|
|
h = d._impl.efc_J.T @ cm @ d._impl.efc_J
|
|
else:
|
|
h = (d._impl.efc_J.T * d._impl.efc_D * ctx.active) @ d._impl.efc_J
|
|
h = support.full_m(m, d) + h
|
|
# Symmetrize to reduce the chance of numerical issues in cholesky.
|
|
h_sym = (h + h.T) * 0.5
|
|
h_ = jax.scipy.linalg.cho_factor(h_sym)
|
|
mgrad = jax.scipy.linalg.cho_solve(h_, grad)
|
|
else:
|
|
raise NotImplementedError(f'unsupported solver type: {m.opt.solver}')
|
|
|
|
ctx = ctx.replace(grad=grad, Mgrad=mgrad)
|
|
|
|
return ctx
|
|
|
|
|
|
def _rescale(m: Model, value: jax.Array) -> jax.Array:
|
|
return value / (m.stat.meaninertia * max(1, m.nv))
|
|
|
|
|
|
def _linesearch(m: Model, d: Data, ctx: Context) -> Context:
|
|
"""Performs a zoom linesearch to find optimal search step size.
|
|
|
|
Args:
|
|
m: model defining search options and other needed terms
|
|
d: data with inertia matrix and other needed terms
|
|
ctx: current solver context
|
|
|
|
Returns:
|
|
updated context with new qacc, Ma, Jaref
|
|
"""
|
|
if (
|
|
not isinstance(m._impl, ModelJAX)
|
|
or not isinstance(d._impl, DataJAX)
|
|
or not isinstance(m.opt._impl, OptionJAX)
|
|
):
|
|
raise ValueError('_lineasearch requires JAX backend implementation.')
|
|
|
|
smag = math.norm(ctx.search) * m.stat.meaninertia * max(1, m.nv)
|
|
gtol = m.opt.tolerance * m.opt.ls_tolerance * smag
|
|
|
|
# compute Mv, Jv
|
|
mv = support.mul_m(m, d, ctx.search)
|
|
jv = d._impl.efc_J @ ctx.search
|
|
|
|
# prepare quadratics
|
|
quad_gauss = jp.stack((
|
|
ctx.gauss,
|
|
jp.dot(ctx.search, ctx.Ma) - jp.dot(ctx.search, d.qfrc_smooth),
|
|
0.5 * jp.dot(ctx.search, mv),
|
|
))
|
|
quad = jp.stack((0.5 * ctx.Jaref * ctx.Jaref, jv * ctx.Jaref, 0.5 * jv * jv))
|
|
quad = (quad * d._impl.efc_D).T
|
|
uu, v0, uv, vv = 0.0, 0.0, 0.0, 0.0
|
|
if m.opt.cone == ConeType.ELLIPTIC:
|
|
mask = d._impl.contact.dim > 1
|
|
# complete vector quadratic (for bottom zone)
|
|
efc_con, efc_fri = [], []
|
|
for condim, addr in zip(
|
|
d._impl.contact.dim[mask], d._impl.contact.efc_address[mask]
|
|
):
|
|
efc_con.extend([addr] * (condim - 1))
|
|
efc_fri.extend(range(addr + 1, addr + condim))
|
|
quad = quad.at[jp.array(efc_con)].add(quad[jp.array(efc_fri)])
|
|
|
|
# rescale to make primal cone circular
|
|
# to prevent out of range append zeros to jv
|
|
jv_fn = jax.vmap(
|
|
lambda x: jax.lax.dynamic_slice(
|
|
jp.concatenate((jv, jp.zeros(3))), (x,), (6,)
|
|
)
|
|
)
|
|
efc_elliptic = d._impl.contact.efc_address[mask]
|
|
v = jv_fn(efc_elliptic) * ctx.fri
|
|
uu = jp.sum(ctx.u[:, 1:] * ctx.u[:, 1:], axis=1)
|
|
v0 = v[:, 0]
|
|
uv = jp.sum(ctx.u[:, 1:] * v[:, 1:], axis=1)
|
|
vv = jp.sum(v[:, 1:] * v[:, 1:], axis=1)
|
|
|
|
point_fn = lambda a: _LSPoint.create(
|
|
m, d, ctx, a, jv, quad, quad_gauss, uu, v0, uv, vv
|
|
)
|
|
|
|
def cond(ctx: _LSContext) -> jax.Array:
|
|
done = ctx.ls_iter >= m.opt.ls_iterations
|
|
done |= ~ctx.swap # if we did not adjust the interval
|
|
done |= (ctx.lo.deriv_0 < 0) & (ctx.lo.deriv_0 > -gtol)
|
|
done |= (ctx.hi.deriv_0 > 0) & (ctx.hi.deriv_0 < gtol)
|
|
|
|
return ~done
|
|
|
|
def body(ctx: _LSContext) -> _LSContext:
|
|
# always compute new bracket boundaries and a midpoint
|
|
lo, hi = ctx.lo, ctx.hi
|
|
lo_next = point_fn(lo.alpha - lo.deriv_0 / lo.deriv_1)
|
|
hi_next = point_fn(hi.alpha - hi.deriv_0 / hi.deriv_1)
|
|
mid = point_fn(0.5 * (lo.alpha + hi.alpha))
|
|
|
|
# swap lo/hi if the derivative points to a narrower bracket width
|
|
in_bracket = lambda x, y: ((x < y) & (y < 0) | (x > y) & (y > 0))
|
|
swap_lo_next = in_bracket(lo.deriv_0, lo_next.deriv_0)
|
|
lo = jax.tree_util.tree_map(
|
|
lambda x, y: jp.where(swap_lo_next, y, x), lo, lo_next
|
|
)
|
|
swap_lo_mid = in_bracket(lo.deriv_0, mid.deriv_0)
|
|
lo = jax.tree_util.tree_map(
|
|
lambda x, y: jp.where(swap_lo_mid, y, x), lo, mid
|
|
)
|
|
swap_lo_hi_next = in_bracket(lo.deriv_0, hi_next.deriv_0)
|
|
lo = jax.tree_util.tree_map(
|
|
lambda x, y: jp.where(swap_lo_hi_next, y, x), lo, hi_next
|
|
)
|
|
swap_hi_next = in_bracket(hi.deriv_0, hi_next.deriv_0)
|
|
hi = jax.tree_util.tree_map(
|
|
lambda x, y: jp.where(swap_hi_next, y, x), hi, hi_next
|
|
)
|
|
swap_hi_mid = in_bracket(hi.deriv_0, mid.deriv_0)
|
|
hi = jax.tree_util.tree_map(
|
|
lambda x, y: jp.where(swap_hi_mid, y, x), hi, mid
|
|
)
|
|
swap_hi_lo_next = in_bracket(hi.deriv_0, lo_next.deriv_0)
|
|
hi = jax.tree_util.tree_map(
|
|
lambda x, y: jp.where(swap_hi_lo_next, y, x), hi, lo_next
|
|
)
|
|
swap = swap_lo_next | swap_lo_mid | swap_lo_hi_next
|
|
swap = swap | swap_hi_next | swap_hi_mid | swap_hi_lo_next
|
|
ctx = ctx.replace(lo=lo, hi=hi, swap=swap, ls_iter=ctx.ls_iter + 1)
|
|
|
|
return ctx
|
|
|
|
# initialize interval
|
|
p0 = point_fn(jp.array(0.0))
|
|
lo = point_fn(p0.alpha - p0.deriv_0 / p0.deriv_1)
|
|
lesser_fn = lambda x, y: jp.where(lo.deriv_0 < p0.deriv_0, x, y)
|
|
hi = jax.tree_util.tree_map(lesser_fn, p0, lo)
|
|
lo = jax.tree_util.tree_map(lesser_fn, lo, p0)
|
|
ls_ctx = _LSContext(lo=lo, hi=hi, swap=jp.array(True), ls_iter=0)
|
|
ls_ctx = _while_loop_scan(cond, body, ls_ctx, m.opt.ls_iterations)
|
|
|
|
# move to new solution if improved
|
|
lo, hi = ls_ctx.lo, ls_ctx.hi
|
|
improved = (lo.cost < p0.cost) | (hi.cost < p0.cost)
|
|
alpha = jp.where(lo.cost < hi.cost, lo.alpha, hi.alpha)
|
|
qacc = ctx.qacc + improved * ctx.search * alpha
|
|
ma = ctx.Ma + improved * mv * alpha
|
|
jaref = ctx.Jaref + improved * jv * alpha
|
|
|
|
ctx = ctx.replace(qacc=qacc, Ma=ma, Jaref=jaref)
|
|
|
|
return ctx
|
|
|
|
|
|
def solve(m: Model, d: Data) -> Data:
|
|
"""Finds forces that satisfy constraints using conjugate gradient descent."""
|
|
if not isinstance(m.opt._impl, OptionJAX):
|
|
raise ValueError('solve requires JAX backend implementation.')
|
|
|
|
def cond(ctx: Context) -> jax.Array:
|
|
improvement = _rescale(m, ctx.prev_cost - ctx.cost)
|
|
gradient = _rescale(m, math.norm(ctx.grad))
|
|
|
|
done = ctx.solver_niter >= m.opt.iterations
|
|
done |= improvement < m.opt.tolerance
|
|
done |= gradient < m.opt.tolerance
|
|
|
|
return ~done
|
|
|
|
def body(ctx: Context) -> Context:
|
|
ctx = _linesearch(m, d, ctx)
|
|
prev_grad, prev_Mgrad = ctx.grad, ctx.Mgrad # pylint: disable=invalid-name
|
|
ctx = _update_constraint(m, d, ctx)
|
|
ctx = _update_gradient(m, d, ctx)
|
|
|
|
if m.opt.solver == SolverType.NEWTON:
|
|
search = -ctx.Mgrad
|
|
else:
|
|
# polak-ribiere:
|
|
beta = jp.dot(ctx.grad, ctx.Mgrad - prev_Mgrad)
|
|
beta = beta / jp.maximum(mujoco.mjMINVAL, jp.dot(prev_grad, prev_Mgrad))
|
|
beta = jp.maximum(0, beta)
|
|
search = -ctx.Mgrad + beta * ctx.search
|
|
ctx = ctx.replace(search=search, solver_niter=ctx.solver_niter + 1)
|
|
|
|
return ctx
|
|
|
|
# warmstart:
|
|
qacc = d.qacc_smooth
|
|
if not m.opt.disableflags & DisableBit.WARMSTART:
|
|
warm = Context.create(m, d.replace(qacc=d.qacc_warmstart), grad=False)
|
|
smth = Context.create(m, d.replace(qacc=d.qacc_smooth), grad=False)
|
|
qacc = jp.where(warm.cost < smth.cost, d.qacc_warmstart, d.qacc_smooth)
|
|
d = d.replace(qacc=qacc)
|
|
|
|
ctx = Context.create(m, d)
|
|
if m.opt.iterations == 1:
|
|
ctx = body(ctx)
|
|
else:
|
|
ctx = jax.lax.while_loop(cond, body, ctx)
|
|
|
|
d = d.tree_replace({
|
|
'qfrc_constraint': ctx.qfrc_constraint,
|
|
'qacc': ctx.qacc,
|
|
'_impl.efc_force': ctx.efc_force,
|
|
})
|
|
|
|
return d
|