use phyz_collision::Collision;
use phyz_contact::gradient::{FixedPointSensitivity, friction_sensitivity};
use phyz_contact::{
ContactAssembly, ContactCache, ContactMaterial, ContactSolution, ContactSolverConfig,
GroundSupport, assemble, cylinder_rim_dir, find_contacts,
find_ground_contacts_model_with_support, regularization_diag, solve_contacts_warm,
};
use phyz_math::{DVec, Vec3};
use phyz_model::{Model, State};
use phyz_rigid::{
aba, forward_kinematics, integrate_configuration, rotate_free_joint_velocities,
strip_free_joint_coriolis,
};
use crate::rollout::FinalStateObjective;
pub use crate::rollout::N_INERTIA_PARAMS;
use crate::rollout::inertia_params;
const FD_EPS: f64 = 1e-8;
pub struct ConvexContactRollout<'a> {
pub model: &'a Model,
pub ground_height: f64,
pub material: ContactMaterial,
pub config: ContactSolverConfig,
pub q0: DVec,
pub v0: DVec,
pub steps: usize,
pub ctrl: &'a dyn Fn(usize) -> DVec,
}
pub struct ConvexAdjointGradients {
pub objective: f64,
pub d_q0: DVec,
pub d_v0: DVec,
pub d_ctrl: Vec<DVec>,
pub d_inertia: Vec<[f64; N_INERTIA_PARAMS]>,
pub d_friction: f64,
pub d_restitution: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConvexAdjointError {
Unconverged {
step: usize,
iterations: usize,
residual: f64,
},
SingularKkt {
step: usize,
},
#[deprecated(note = "body-body contacts are supported; this variant is never \
constructed and will be removed in the next minor release")]
BodyBodyContact {
step: usize,
},
UnsupportedGeometry {
step: usize,
body: usize,
},
}
impl core::fmt::Display for ConvexAdjointError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Unconverged {
step,
iterations,
residual,
} => write!(
f,
"contact solve at step {step} did not converge \
({iterations} iterations, residual {residual:.3e}); \
refusing to differentiate a non-KKT point"
),
Self::SingularKkt { step } => write!(
f,
"KKT matrix at step {step} is singular at the converged \
active set; the derivative does not exist there"
),
#[allow(deprecated)]
Self::BodyBodyContact { step } => write!(
f,
"body-body contact at step {step}: this refusal is retired — \
body-body contacts are differentiated"
),
Self::UnsupportedGeometry { step, body } => write!(
f,
"could not anchor a ground contact on body {body} at step \
{step}: unsupported geometry for the frozen-contact lanes"
),
}
}
}
impl std::error::Error for ConvexAdjointError {}
#[derive(Debug, Clone, Copy)]
enum Anchor {
Ground {
body: usize,
material_point: Vec3,
world_offset: Vec3,
},
GroundCylinderRim {
body: usize,
cap_centre: Vec3,
axis_local: Vec3,
radius: f64,
quarter_turns: u8,
frozen_offset: Vec3,
},
Pair {
body_i: usize,
body_j: usize,
point_i: Vec3,
point_j: Vec3,
normal_frame: usize,
normal_local: Vec3,
radius_i: Option<f64>,
radius_j: Option<f64>,
},
}
fn sphere_radius(model: &Model, body: usize) -> Option<(f64, Vec3)> {
use phyz_model::Geometry;
let b = model.bodies.get(body)?;
if b.collisions.is_empty() {
match b.geometry {
Some(Geometry::Sphere { radius }) => Some((radius, Vec3::zeros())),
_ => None,
}
} else if b.collisions.len() == 1 {
match b.collisions[0].geometry {
Geometry::Sphere { radius } => Some((radius, b.collisions[0].origin.pos)),
_ => None,
}
} else {
None
}
}
impl Anchor {
fn of(
c: &Collision,
support_kind: GroundSupport,
state: &State,
ground_height: f64,
model: &Model,
) -> Self {
if c.is_world_j() {
let xform = &state.body_xform[c.body_i];
let support = Vec3::new(
c.contact_point.x,
c.contact_point.y,
ground_height - c.penetration_depth,
);
let world_offset = support_kind.world_offset();
let material_point = xform.rot * (support - xform.pos - world_offset);
if let GroundSupport::CylinderRim {
axis,
radius,
quarter_turns,
offset,
} = support_kind
{
return Self::GroundCylinderRim {
body: c.body_i,
cap_centre: material_point,
axis_local: xform.rot * axis,
radius,
quarter_turns,
frozen_offset: offset,
};
}
return Self::Ground {
body: c.body_i,
material_point,
world_offset,
};
}
let n = c.contact_normal;
let half = 0.5 * c.penetration_depth;
let surface_i = c.contact_point - n * half;
let surface_j = c.contact_point + n * half;
let xi = &state.body_xform[c.body_i];
let xj = &state.body_xform[c.body_j];
let normal_frame = Self::reference_body(c.body_i, c.body_j, n, state, model);
let owner = &state.body_xform[normal_frame];
let sph_i = sphere_radius(model, c.body_i);
let sph_j = sphere_radius(model, c.body_j);
Self::Pair {
body_i: c.body_i,
body_j: c.body_j,
point_i: sph_i
.map(|(_, centre)| centre)
.unwrap_or(xi.rot * (surface_i - xi.pos)),
point_j: sph_j
.map(|(_, centre)| centre)
.unwrap_or(xj.rot * (surface_j - xj.pos)),
normal_frame,
normal_local: owner.rot * n,
radius_i: sph_i.map(|(r, _)| r),
radius_j: sph_j.map(|(r, _)| r),
}
}
fn reference_body(
body_i: usize,
body_j: usize,
n_world: Vec3,
state: &State,
model: &Model,
) -> usize {
match (sphere_radius(model, body_i), sphere_radius(model, body_j)) {
(Some(_), None) => return body_j,
(None, Some(_)) => return body_i,
_ => {}
}
let axis_alignment = |body: usize| -> f64 {
let local = state.body_xform[body].rot * n_world;
local.x.abs().max(local.y.abs()).max(local.z.abs())
};
if axis_alignment(body_j) > axis_alignment(body_i) {
body_j
} else {
body_i
}
}
fn collision(&self, state: &State, ground_height: f64) -> Collision {
match *self {
Self::Ground {
body,
material_point,
world_offset,
} => {
let xform = &state.body_xform[body];
let support = xform.pos + xform.rot.transpose() * material_point + world_offset;
let depth = ground_height - support.z;
Collision {
body_i: body,
body_j: Collision::WORLD,
contact_point: Vec3::new(support.x, support.y, ground_height - depth * 0.5),
contact_normal: Vec3::z(),
penetration_depth: depth,
}
}
Self::GroundCylinderRim {
body,
cap_centre,
axis_local,
radius,
quarter_turns,
frozen_offset,
} => {
let xform = &state.body_xform[body];
let axis = xform.rot.transpose() * axis_local;
let offset =
cylinder_rim_dir(axis, quarter_turns).map_or(frozen_offset, |dir| dir * radius);
let support = xform.pos + xform.rot.transpose() * cap_centre + offset;
let depth = ground_height - support.z;
Collision {
body_i: body,
body_j: Collision::WORLD,
contact_point: Vec3::new(support.x, support.y, ground_height - depth * 0.5),
contact_normal: Vec3::z(),
penetration_depth: depth,
}
}
Self::Pair {
body_i,
body_j,
point_i,
point_j,
normal_frame,
normal_local,
radius_i,
radius_j,
} => {
let xi = &state.body_xform[body_i];
let xj = &state.body_xform[body_j];
let n = state.body_xform[normal_frame].rot.transpose() * normal_local;
let mut pi = xi.pos + xi.rot.transpose() * point_i;
let mut pj = xj.pos + xj.rot.transpose() * point_j;
if let Some(r) = radius_i {
pi -= n * r;
}
if let Some(r) = radius_j {
pj += n * r;
}
let depth = (pj - pi).dot(n);
let vertex = if normal_frame == body_i { pj } else { pi };
let sign = if normal_frame == body_i { -1.0 } else { 1.0 };
Collision {
body_i,
body_j,
contact_point: vertex + n * (sign * 0.5 * depth),
contact_normal: n,
penetration_depth: depth,
}
}
}
}
}
struct StepRecord {
q: DVec,
v: DVec,
u: DVec,
v_next: DVec,
anchors: Vec<Anchor>,
contact: Option<(ContactAssembly, ContactSolution)>,
}
type StepSolve = Option<(ContactAssembly, ContactSolution)>;
fn forward_step(
model: &Model,
state: &mut State,
ground_height: f64,
material: &ContactMaterial,
config: &ContactSolverConfig,
cache: &mut ContactCache,
) -> (Vec<(Collision, GroundSupport)>, StepSolve) {
let dt = model.dt;
let (xforms, _velocities) = forward_kinematics(model, state);
state.body_xform = xforms;
let mut contacts: Vec<(Collision, GroundSupport)> =
find_ground_contacts_model_with_support(model, state, ground_height, material.margin);
let body_contacts = find_contacts(model, state, material.margin);
contacts.extend(
body_contacts
.into_iter()
.map(|c| (c, GroundSupport::Material)),
);
let mut qdd = aba(model, state);
let v_before = state.v.clone();
strip_free_joint_coriolis(model, v_before.as_slice(), qdd.as_mut_slice());
let free_qd = &state.v + &(&qdd * dt);
let bare: Vec<Collision> = contacts.iter().map(|(c, _)| c.clone()).collect();
let record = if contacts.is_empty() {
state.v = free_qd;
None
} else {
let materials = model.contact_materials(material);
let asm = assemble(model, state, &bare, &materials, &free_qd, dt, config);
let seed = cache.warm_start(state, &bare);
let solution = solve_contacts_warm(&asm.problem, config, &seed);
cache.store(state, &bare, &solution.impulses);
state.v = &free_qd + &asm.velocity_delta(&solution.impulses);
Some((asm, solution))
};
rotate_free_joint_velocities(model, v_before.as_slice(), state.v.as_mut_slice(), dt);
let v_clone = state.v.clone();
integrate_configuration(model, state.q.as_mut_slice(), v_clone.as_slice(), dt);
state.time += dt;
let (xforms, _) = forward_kinematics(model, state);
state.body_xform = xforms;
(contacts, record)
}
fn forward_rollout(
rollout: &ConvexContactRollout,
) -> Result<(Vec<StepRecord>, State), ConvexAdjointError> {
let model = rollout.model;
let mut state = model.default_state();
state.q = rollout.q0.clone();
state.v = rollout.v0.clone();
let mut cache = ContactCache::default();
let mut records = Vec::with_capacity(rollout.steps);
for t in 0..rollout.steps {
let q = state.q.clone();
let v = state.v.clone();
let u = (rollout.ctrl)(t);
state.ctrl = u.clone();
let (xforms, _) = forward_kinematics(model, &state);
state.body_xform = xforms;
let (contacts, contact) = forward_step(
model,
&mut state,
rollout.ground_height,
&rollout.material,
&rollout.config,
&mut cache,
);
let mut anchors = Vec::with_capacity(contacts.len());
let mut pre = model.default_state();
pre.q = q.clone();
pre.v = v.clone();
let (pre_xf, _) = forward_kinematics(model, &pre);
pre.body_xform = pre_xf;
for (c, support) in &contacts {
anchors.push(Anchor::of(c, *support, &pre, rollout.ground_height, model));
}
records.push(StepRecord {
q,
v,
u,
v_next: state.v.clone(),
anchors,
contact,
});
}
Ok((records, state))
}
pub fn convex_rollout_objective(
rollout: &ConvexContactRollout,
objective: &FinalStateObjective,
) -> f64 {
let model = rollout.model;
let mut state = model.default_state();
state.q = rollout.q0.clone();
state.v = rollout.v0.clone();
let mut cache = ContactCache::default();
for t in 0..rollout.steps {
state.ctrl = (rollout.ctrl)(t);
forward_step(
model,
&mut state,
rollout.ground_height,
&rollout.material,
&rollout.config,
&mut cache,
);
}
(objective.value)(state.q.as_slice(), state.v.as_slice())
}
struct Pieces {
v_free: DVec,
residual: Vec<f64>,
mt_rel: Vec<[f64; 2]>,
gf: DVec,
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::needless_range_loop)]
fn eval_pieces(
model: &Model,
ground_height: f64,
material: &ContactMaterial,
config: &ContactSolverConfig,
anchors: &[Anchor],
f_star: &[Vec3],
t_rel: &[[f64; 2]],
q: &DVec,
v: &DVec,
u: &DVec,
) -> Pieces {
let dt = model.dt;
let mut state = model.default_state();
state.q = q.clone();
state.v = v.clone();
state.ctrl = u.clone();
let (xforms, _) = forward_kinematics(model, &state);
state.body_xform = xforms;
let mut qdd = aba(model, &state);
strip_free_joint_coriolis(model, state.v.as_slice(), qdd.as_mut_slice());
let v_free = &state.v + &(&qdd * dt);
if anchors.is_empty() {
return Pieces {
v_free,
residual: Vec::new(),
mt_rel: Vec::new(),
gf: DVec::zeros(model.nv),
};
}
let contacts: Vec<Collision> = anchors
.iter()
.map(|a| a.collision(&state, ground_height))
.collect();
let materials = model.contact_materials(material);
let asm = assemble(model, &state, &contacts, &materials, &v_free, dt, config);
let p = &asm.problem;
let n = p.n;
let dim = 3 * n;
let mut flat = vec![0.0; dim];
for (c, f) in f_star.iter().enumerate() {
flat[3 * c] = f.x;
flat[3 * c + 1] = f.y;
flat[3 * c + 2] = f.z;
}
let mut residual = vec![0.0; dim];
for c in 0..n {
let reg = regularization_diag(p, c, config);
let base = 3 * c;
for r in 0..3 {
let mut acc = p.free_velocity[base + r];
for (col, fc) in flat.iter().enumerate() {
acc += p.delassus[(base + r) * dim + col] * fc;
}
acc += reg[r] * flat[base + r];
if r == 0 {
acc -= p.rows[c].bias;
}
residual[base + r] = acc;
}
}
let mut mt_rel = vec![[0.0f64; 2]; n];
for (c, tr) in t_rel.iter().enumerate() {
let base = 3 * c;
for i in 0..2 {
mt_rel[c][i] = p.delassus[(base + 1 + i) * dim + base + 1] * tr[0]
+ p.delassus[(base + 1 + i) * dim + base + 2] * tr[1];
}
}
let gf = asm.velocity_delta(f_star);
Pieces {
v_free,
residual,
mt_rel,
gf,
}
}
fn rotate_tangent(model: &Model, v: &DVec, v_next: &DVec, dv: &mut DVec, v_lane: Option<usize>) {
let dt = model.dt;
for (jidx, joint) in model.joints.iter().enumerate() {
if joint.joint_type != phyz_model::JointType::Free {
continue;
}
let off = model.v_offsets[jidx];
let omega = Vec3::new(v[off], v[off + 1], v[off + 2]);
let d = Vec3::new(dv[off + 3], dv[off + 4], dv[off + 5]);
let mut out = phyz_math::quat_exp(&(omega * -dt)).rotate(d);
if let Some(j) = v_lane
&& j >= off
&& j < off + 3
{
let domega = match j - off {
0 => Vec3::new(1.0, 0.0, 0.0),
1 => Vec3::new(0.0, 1.0, 0.0),
_ => Vec3::new(0.0, 0.0, 1.0),
};
let after = Vec3::new(v_next[off + 3], v_next[off + 4], v_next[off + 5]);
let before = phyz_math::quat_exp(&(omega * dt)).rotate(after);
let h = 1e-6;
let p = phyz_math::quat_exp(&((omega + domega * h) * -dt)).rotate(before);
let m = phyz_math::quat_exp(&((omega - domega * h) * -dt)).rotate(before);
out += (p - m) * (1.0 / (2.0 * h));
}
dv[off + 3] = out.x;
dv[off + 4] = out.y;
dv[off + 5] = out.z;
}
}
fn phi(model: &Model, q: &DVec, v_next: &DVec) -> DVec {
let mut out = q.clone();
integrate_configuration(model, out.as_mut_slice(), v_next.as_slice(), model.dt);
out
}
fn perturbed_model(model: &Model, body: usize, params: &[f64; N_INERTIA_PARAMS]) -> Model {
use phyz_math::{Mat3, SpatialInertia};
let mut m = model.clone();
let p = params;
m.bodies[body].inertia = SpatialInertia::new(
p[0],
Vec3::new(p[1], p[2], p[3]),
Mat3::new(p[4], p[7], p[8], p[7], p[5], p[9], p[8], p[9], p[6]),
);
m
}
pub fn convex_adjoint_gradient(
rollout: &ConvexContactRollout,
objective: &FinalStateObjective,
) -> Result<ConvexAdjointGradients, ConvexAdjointError> {
let model = rollout.model;
let nq = model.nq;
let nv = model.nv;
let nb = model.bodies.len();
let (records, final_state) = forward_rollout(rollout)?;
let j0 = (objective.value)(final_state.q.as_slice(), final_state.v.as_slice());
let (gq, gv) = (objective.gradient)(final_state.q.as_slice(), final_state.v.as_slice());
assert_eq!(gq.len(), nq, "objective ∂g/∂q length");
assert_eq!(gv.len(), nv, "objective ∂g/∂v length");
let mut lam_q = DVec::from_slice(&gq);
let mut lam_v = DVec::from_slice(&gv);
let mut d_ctrl = vec![DVec::zeros(nv); rollout.steps];
let mut d_inertia = vec![[0.0f64; N_INERTIA_PARAMS]; nb];
let mut d_friction = 0.0f64;
let mut d_restitution = 0.0f64;
let nominal_params: Vec<[f64; N_INERTIA_PARAMS]> = model
.bodies
.iter()
.map(|b| inertia_params(&b.inertia))
.collect();
for (t, rec) in records.iter().enumerate().rev() {
let fps: Option<FixedPointSensitivity> = match &rec.contact {
None => None,
Some((asm, sol)) => {
if !sol.converged {
return Err(ConvexAdjointError::Unconverged {
step: t,
iterations: sol.iterations,
residual: sol.residual,
});
}
Some(
FixedPointSensitivity::at(&asm.problem, sol, &rollout.config)
.ok_or(ConvexAdjointError::SingularKkt { step: t })?,
)
}
};
let t_rel: Vec<[f64; 2]> = match (&rec.contact, &fps) {
(Some((asm, _)), Some(s)) => (0..asm.problem.n)
.map(|c| s.slide_tangent(c).map(|st| st.t_rel).unwrap_or([0.0; 2]))
.collect(),
_ => Vec::new(),
};
let f_star: Vec<Vec3> = rec
.contact
.as_ref()
.map(|(_, s)| s.impulses.clone())
.unwrap_or_default();
let dv_next = |dp: &Pieces, v_lane: Option<usize>| -> DVec {
let mut dvn = match (&rec.contact, &fps) {
(Some((asm, _)), Some(s)) => {
let df = s.apply(&dp.residual, &dp.mt_rel);
&(&dp.v_free + &dp.gf) + &asm.velocity_delta(&df)
}
_ => dp.v_free.clone(),
};
rotate_tangent(model, &rec.v, &rec.v_next, &mut dvn, v_lane);
dvn
};
let eval = |m: &Model, mat: &ContactMaterial, q: &DVec, v: &DVec, u: &DVec| -> Pieces {
eval_pieces(
m,
rollout.ground_height,
mat,
&rollout.config,
&rec.anchors,
&f_star,
&t_rel,
q,
v,
u,
)
};
#[allow(clippy::too_many_arguments)]
let lane = |mp: &Model,
mm: &Model,
matp: &ContactMaterial,
matm: &ContactMaterial,
qp: &DVec,
qm: &DVec,
vp: &DVec,
vm: &DVec,
up: &DVec,
um: &DVec,
h: f64|
-> Pieces {
let a = eval(mp, matp, qp, vp, up);
let b = eval(mm, matm, qm, vm, um);
let inv = 1.0 / (2.0 * h);
Pieces {
v_free: &(&a.v_free - &b.v_free) * inv,
residual: a
.residual
.iter()
.zip(&b.residual)
.map(|(x, y)| (x - y) * inv)
.collect(),
mt_rel: a
.mt_rel
.iter()
.zip(&b.mt_rel)
.map(|(x, y)| [(x[0] - y[0]) * inv, (x[1] - y[1]) * inv])
.collect(),
gf: &(&a.gf - &b.gf) * inv,
}
};
let dphi_dvnext = |dvn: &DVec| -> DVec {
let norm = dvn.as_slice().iter().fold(0.0f64, |m, x| m.max(x.abs()));
if norm == 0.0 {
return DVec::zeros(nq);
}
let s = FD_EPS / norm;
let vp = &rec.v_next + &(dvn * s);
let vm = &rec.v_next - &(dvn * s);
&(&phi(model, &rec.q, &vp) - &phi(model, &rec.q, &vm)) * (1.0 / (2.0 * s))
};
let contract = |dqn: &DVec, dvn: &DVec| -> f64 {
let mut acc = 0.0;
for i in 0..nq {
acc += lam_q[i] * dqn[i];
}
for j in 0..nv {
acc += lam_v[j] * dvn[j];
}
acc
};
let mut new_lam_q = DVec::zeros(nq);
let mut new_lam_v = DVec::zeros(nv);
for i in 0..nq {
let h = FD_EPS * rec.q[i].abs().max(1.0);
let mut qp = rec.q.clone();
let mut qm = rec.q.clone();
qp[i] += h;
qm[i] -= h;
let dp = lane(
model,
model,
&rollout.material,
&rollout.material,
&qp,
&qm,
&rec.v,
&rec.v,
&rec.u,
&rec.u,
h,
);
let dvn = dv_next(&dp, None);
let dqn_direct = &(&phi(model, &qp, &rec.v_next) - &phi(model, &qm, &rec.v_next))
* (1.0 / (2.0 * h));
let dqn = &dqn_direct + &dphi_dvnext(&dvn);
new_lam_q[i] = contract(&dqn, &dvn);
}
for j in 0..nv {
let h = FD_EPS * rec.v[j].abs().max(1.0);
let mut vp = rec.v.clone();
let mut vm = rec.v.clone();
vp[j] += h;
vm[j] -= h;
let dp = lane(
model,
model,
&rollout.material,
&rollout.material,
&rec.q,
&rec.q,
&vp,
&vm,
&rec.u,
&rec.u,
h,
);
let dvn = dv_next(&dp, Some(j));
let dqn = dphi_dvnext(&dvn);
new_lam_v[j] = contract(&dqn, &dvn);
}
for j in 0..nv {
let h = FD_EPS * rec.u[j].abs().max(1.0);
let mut up = rec.u.clone();
let mut um = rec.u.clone();
up[j] += h;
um[j] -= h;
let dp = lane(
model,
model,
&rollout.material,
&rollout.material,
&rec.q,
&rec.q,
&rec.v,
&rec.v,
&up,
&um,
h,
);
let dvn = dv_next(&dp, None);
let dqn = dphi_dvnext(&dvn);
d_ctrl[t][j] = contract(&dqn, &dvn);
}
for b in 0..nb {
for k in 0..N_INERTIA_PARAMS {
let h = FD_EPS * nominal_params[b][k].abs().max(1.0);
let mut pp = nominal_params[b];
let mut pm = nominal_params[b];
pp[k] += h;
pm[k] -= h;
let mp = perturbed_model(model, b, &pp);
let mm = perturbed_model(model, b, &pm);
let dp = lane(
&mp,
&mm,
&rollout.material,
&rollout.material,
&rec.q,
&rec.q,
&rec.v,
&rec.v,
&rec.u,
&rec.u,
h,
);
let dvn = dv_next(&dp, None);
let dqn = dphi_dvnext(&dvn);
d_inertia[b][k] += contract(&dqn, &dvn);
}
}
{
let h = FD_EPS * rollout.material.restitution.abs().max(1.0);
let mut matp = rollout.material.clone();
let mut matm = rollout.material.clone();
matp.restitution += h;
matm.restitution -= h;
let dp = lane(
model, model, &matp, &matm, &rec.q, &rec.q, &rec.v, &rec.v, &rec.u, &rec.u, h,
);
let dvn = dv_next(&dp, None);
let dqn = dphi_dvnext(&dvn);
d_restitution += contract(&dqn, &dvn);
}
if let Some((asm, sol)) = &rec.contact {
let n = asm.problem.n;
if let Some(dfdmu) = friction_sensitivity(&asm.problem, sol, &rollout.config) {
let mut df = vec![Vec3::zeros(); n];
for (c, dfc) in df.iter_mut().enumerate() {
let base = 3 * c;
let mut acc = [0.0f64; 3];
for (r, a) in acc.iter_mut().enumerate() {
for col in 0..n {
*a += dfdmu[(base + r) * n + col];
}
}
*dfc = Vec3::new(acc[0], acc[1], acc[2]);
}
let dvn = asm.velocity_delta(&df);
let dqn = dphi_dvnext(&dvn);
d_friction += contract(&dqn, &dvn);
}
}
lam_q = new_lam_q;
lam_v = new_lam_v;
}
Ok(ConvexAdjointGradients {
objective: j0,
d_q0: lam_q,
d_v0: lam_v,
d_ctrl,
d_inertia,
d_friction,
d_restitution,
})
}