use super::step::{
CollisionMesh, GroundContact, N_INERTIA_PARAMS, aba_generic, config_update_generic, fk_generic,
inertia_from_params, joint_nq, lift_inertia, lift_vec3, nominal_motion, rollout_states,
step_generic, validate_and_params, vertex_wrench,
};
use phyz_math::{DVec, Vec3};
use phyz_model::Model;
use tang::{Dual, Mat3 as GMat3, SpatialInertia, SpatialMat, SpatialVec, Vec3 as GVec3};
type D = Dual<f64>;
fn inertia_param_derivatives(
params: &[f64; N_INERTIA_PARAMS],
) -> [SpatialMat<f64>; N_INERTIA_PARAMS] {
let dual_block = |m: &GMat3<D>| {
GMat3::new(
m.get(0, 0).dual,
m.get(0, 1).dual,
m.get(0, 2).dual,
m.get(1, 0).dual,
m.get(1, 1).dual,
m.get(1, 2).dual,
m.get(2, 0).dual,
m.get(2, 1).dual,
m.get(2, 2).dual,
)
};
core::array::from_fn(|k| {
let mut p: [D; N_INERTIA_PARAMS] = core::array::from_fn(|i| D::constant(params[i]));
p[k] = D::var(params[k]);
let m = inertia_from_params(&p).to_matrix();
SpatialMat::new(
dual_block(&m.upper_left),
dual_block(&m.upper_right),
dual_block(&m.lower_left),
dual_block(&m.lower_right),
)
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Seed {
Position,
Velocity,
}
struct LaneGroup<'a> {
model: &'a Model,
layout: &'a super::step::DofLayout,
contact: Option<(&'a GroundContact, &'a [CollisionMesh])>,
q: &'a [f64],
v: &'a [f64],
ctrl: &'a [f64],
w: &'a [f64],
seed: Seed,
chunks: &'a [(usize, usize)],
}
impl crate::multidual::LaneOp for LaneGroup<'_> {
type Out = Vec<f64>;
fn call<const N: usize>(self) -> Vec<f64> {
type M<const N: usize> = crate::multidual::MultiDual<N>;
let lift = |xs: &[f64]| -> Vec<M<N>> { xs.iter().map(|&x| M::<N>::constant(x)).collect() };
let q_c = lift(self.q);
let v_c = lift(self.v);
let ctrl_d = lift(self.ctrl);
let inertias: Vec<SpatialInertia<M<N>>> = self
.model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let mut out = Vec::new();
for &(start, count) in self.chunks {
let (mut q_d, mut v_d) = (q_c.clone(), v_c.clone());
for lane in 0..count {
let idx = start + lane;
match self.seed {
Seed::Position => q_d[idx] = M::<N>::var(self.q[idx], lane),
Seed::Velocity => v_d[idx] = M::<N>::var(self.v[idx], lane),
}
}
let (_, _, qdd) = step_generic(
self.model,
self.layout,
&inertias,
self.contact,
None,
&q_d,
&v_d,
&ctrl_d,
);
out.extend((0..count).map(|lane| -> f64 {
qdd.iter()
.zip(self.w)
.map(|(a, &wi)| a.dual[lane] * wi)
.sum()
}));
}
out
}
}
fn chunk_plan(n: usize) -> Vec<(usize, usize, usize)> {
let mut plan = Vec::new();
let mut start = 0;
while start < n {
let remaining = n - start;
let width = crate::multidual::width_for(remaining);
let count = remaining.min(width);
plan.push((width, start, count));
start += count;
}
plan
}
#[allow(clippy::too_many_arguments)]
fn state_lanes(
model: &Model,
layout: &super::step::DofLayout,
contact: Option<(&GroundContact, &[CollisionMesh])>,
inertias_dual: &[SpatialInertia<D>],
q: &[f64],
v: &[f64],
ctrl: &[f64],
w: &[f64],
seed: Seed,
) -> Vec<f64> {
let n = match seed {
Seed::Position => layout.nq,
Seed::Velocity => layout.nv,
};
if n == 1 {
let lift = |xs: &[f64]| -> Vec<D> { xs.iter().map(|&x| D::constant(x)).collect() };
let (mut q_d, mut v_d, u_d) = (lift(q), lift(v), lift(ctrl));
match seed {
Seed::Position => q_d[0] = D::var(q[0]),
Seed::Velocity => v_d[0] = D::var(v[0]),
}
let (_, _, qdd) = step_generic(
model,
layout,
inertias_dual,
contact,
None,
&q_d,
&v_d,
&u_d,
);
return vec![qdd.iter().zip(w).map(|(a, &wi)| a.dual * wi).sum()];
}
let plan = chunk_plan(n);
let mut out = Vec::with_capacity(n);
let mut done = vec![false; plan.len()];
for i in 0..plan.len() {
if done[i] {
continue;
}
let width = plan[i].0;
let mut chunks: Vec<(usize, usize)> = Vec::new();
for (j, c) in plan.iter().enumerate() {
if c.0 == width && !done[j] {
done[j] = true;
chunks.push((c.1, c.2));
}
}
out.extend(crate::multidual::for_lanes(
width,
LaneGroup {
model,
layout,
contact,
q,
v,
ctrl,
w,
seed,
chunks: &chunks,
},
));
}
out
}
pub struct ContactSetup<'a> {
pub ground: GroundContact,
pub meshes: &'a [CollisionMesh],
}
pub struct AdjointRollout<'a> {
pub model: &'a Model,
pub contact: Option<ContactSetup<'a>>,
pub q0: Vec<f64>,
pub v0: Vec<f64>,
pub steps: usize,
pub ctrl: &'a dyn Fn(usize) -> DVec,
}
pub type ObjectiveGradientFn<'a> = &'a dyn Fn(&[f64], &[f64]) -> (Vec<f64>, Vec<f64>);
pub struct FinalStateObjective<'a> {
pub value: &'a dyn Fn(&[f64], &[f64]) -> f64,
pub gradient: ObjectiveGradientFn<'a>,
}
pub struct AdjointGradients {
pub objective: f64,
pub d_inertia: Vec<[f64; N_INERTIA_PARAMS]>,
pub d_vertices: Vec<Vec<Vec3>>,
}
pub fn rollout_objective(rollout: &AdjointRollout, objective: &FinalStateObjective) -> f64 {
let (layout, _) = validate_and_params(rollout.model);
let contact = rollout.contact.as_ref().map(|c| (&c.ground, c.meshes));
let (states, _) = rollout_states(
rollout.model,
&layout,
contact,
&rollout.q0,
&rollout.v0,
rollout.ctrl,
rollout.steps,
);
let (q_t, v_t) = states.last().expect("rollout produced no states");
(objective.value)(q_t, v_t)
}
pub fn adjoint_rollout_gradient(
rollout: &AdjointRollout,
objective: &FinalStateObjective,
) -> AdjointGradients {
let model = rollout.model;
let (layout, params) = validate_and_params(model);
let nb = model.nbodies();
let nq = layout.nq;
let nv = layout.nv;
let dt = model.dt;
let contact = rollout.contact.as_ref().map(|c| (&c.ground, c.meshes));
assert_eq!(rollout.q0.len(), nq, "q0 length (DofLayout::nq)");
assert_eq!(rollout.v0.len(), nv, "v0 length (nv)");
let (states, accels) = rollout_states(
model,
&layout,
contact,
&rollout.q0,
&rollout.v0,
rollout.ctrl,
rollout.steps,
);
let (q_final, v_final) = states.last().expect("rollout produced no states");
let j0 = (objective.value)(q_final, v_final);
let (mut lam_q, mut lam_v) = (objective.gradient)(q_final, v_final);
assert_eq!(lam_q.len(), nq, "objective ∂g/∂q length");
assert_eq!(lam_v.len(), nv, "objective ∂g/∂v length");
let mut d_inertia = vec![[0.0f64; N_INERTIA_PARAMS]; nb];
let mut d_vertices: Vec<Vec<Vec3>> = rollout
.contact
.as_ref()
.map(|c| {
c.meshes
.iter()
.map(|m| vec![Vec3::zeros(); m.vertices.len()])
.collect()
})
.unwrap_or_default();
let inertias_f64: Vec<SpatialInertia<f64>> = model.bodies.iter().map(|b| b.inertia).collect();
let inertias_dual: Vec<SpatialInertia<D>> = model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let d_i_mats: Vec<[SpatialMat<f64>; N_INERTIA_PARAMS]> =
params.iter().map(inertia_param_derivatives).collect();
let model_free = {
let mut m = model.clone();
m.gravity = Vec3::zeros();
m
};
let zeros_v = vec![0.0f64; nv];
let euclidean_q = model
.joints
.iter()
.all(|j| joint_nq(j.joint_type) == j.ndof());
for t in (0..rollout.steps).rev() {
let (q_t, v_t) = &states[t];
let v_next = &states[t + 1].1;
let u_t = (rollout.ctrl)(t);
let u_t = u_t.as_slice();
let lift = |xs: &[f64]| -> Vec<D> { xs.iter().map(|&x| D::constant(x)).collect() };
let q_c = lift(q_t);
let vn_c = lift(v_next);
let dt_d = D::constant(dt);
let phi_dot = |q_d: &[D], vn_d: &[D]| -> f64 {
let qn = config_update_generic(model, &layout, q_d, vn_d, dt_d);
qn.iter().zip(&lam_q).map(|(x, &l)| x.dual * l).sum()
};
let (phi_q, psi) = if euclidean_q {
(lam_q.clone(), lam_q.iter().map(|&l| dt * l).collect())
} else {
let mut phi_q = vec![0.0f64; nq];
for i in 0..nq {
let mut q_d = q_c.clone();
q_d[i] = D::var(q_t[i]);
phi_q[i] = phi_dot(&q_d, &vn_c);
}
let mut psi = vec![0.0f64; nv];
for j in 0..nv {
let mut vn_d = vn_c.clone();
vn_d[j] = D::var(v_next[j]);
psi[j] = phi_dot(&q_c, &vn_d);
}
(phi_q, psi)
};
let w: Vec<f64> = psi.iter().zip(&lam_v).map(|(&p, &lv)| p + lv).collect();
let aq = state_lanes(
model,
&layout,
contact,
&inertias_dual,
q_t,
v_t,
u_t,
&w,
Seed::Position,
);
let av = state_lanes(
model,
&layout,
contact,
&inertias_dual,
q_t,
v_t,
u_t,
&w,
Seed::Velocity,
);
let (vel_b, acc_b) = nominal_motion(model, &layout, q_t, v_t, &accels[t]);
let y = aba_generic::<f64>(&model_free, &layout, &inertias_f64, q_t, &zeros_v, &w, None);
let (_, a_y) = fk_generic::<f64>(model, &layout, q_t, &y);
for b in 0..nb {
for k in 0..N_INERTIA_PARAMS {
let di = &d_i_mats[b][k];
let f = di.mul_vec(&acc_b[b]) + vel_b[b].cross_force(&di.mul_vec(&vel_b[b]));
d_inertia[b][k] -= dt * a_y[b].dot(&f);
}
}
if let Some(setup) = rollout.contact.as_ref() {
let chi: Vec<[f64; 6]> = (0..nb)
.map(|b| {
let m = a_y[b].as_array();
core::array::from_fn(|c| dt * m[c])
})
.collect();
let (xforms, vels) = fk_generic::<f64>(model, &layout, q_t, v_t);
for (mi, mesh) in setup.meshes.iter().enumerate() {
let b = mesh.body;
let xf_d = tang::SpatialTransform::<D>::new(
tang::Mat3::from_cols(
lift_vec3(xforms[b].rot.col(0)),
lift_vec3(xforms[b].rot.col(1)),
lift_vec3(xforms[b].rot.col(2)),
),
lift_vec3(xforms[b].pos),
);
let twist_d =
SpatialVec::new(lift_vec3(vels[b].angular), lift_vec3(vels[b].linear));
for (vi, vtx) in mesh.vertices.iter().enumerate() {
for k in 0..3 {
let mut x_b: GVec3<D> = lift_vec3(*vtx);
match k {
0 => x_b.x = D::var(vtx.x),
1 => x_b.y = D::var(vtx.y),
_ => x_b.z = D::var(vtx.z),
}
let wr = vertex_wrench(&setup.ground, x_b, &xf_d, &twist_d);
let wr = wr.as_array();
let mut acc = 0.0;
for (c, w_c) in wr.iter().enumerate() {
acc += chi[b][c] * w_c.dual;
}
match k {
0 => d_vertices[mi][vi].x += acc,
1 => d_vertices[mi][vi].y += acc,
_ => d_vertices[mi][vi].z += acc,
}
}
}
}
}
for i in 0..nq {
lam_q[i] = phi_q[i] + dt * aq[i];
}
for j in 0..nv {
lam_v[j] = w[j] + dt * av[j];
}
}
AdjointGradients {
objective: j0,
d_inertia,
d_vertices,
}
}
#[cfg(test)]
mod tests {
use super::*;
use phyz_math::{Mat3, SpatialInertia as SI64, SpatialTransform};
use phyz_model::ModelBuilder;
fn state_lanes_scalar(
model: &Model,
layout: &super::super::step::DofLayout,
q: &[f64],
v: &[f64],
ctrl: &[f64],
w: &[f64],
seed: Seed,
) -> Vec<f64> {
let lift = |xs: &[f64]| -> Vec<D> { xs.iter().map(|&x| D::constant(x)).collect() };
let (q_c, v_c, u_c) = (lift(q), lift(v), lift(ctrl));
let inertias: Vec<SpatialInertia<D>> = model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let n = match seed {
Seed::Position => layout.nq,
Seed::Velocity => layout.nv,
};
(0..n)
.map(|k| {
let (mut q_d, mut v_d) = (q_c.clone(), v_c.clone());
match seed {
Seed::Position => q_d[k] = D::var(q[k]),
Seed::Velocity => v_d[k] = D::var(v[k]),
}
let (_, _, qdd) =
step_generic(model, layout, &inertias, None, None, &q_d, &v_d, &u_c);
qdd.iter().zip(w).map(|(a, &wi)| a.dual * wi).sum()
})
.collect()
}
fn link(m: f64) -> SI64 {
SI64::new(
m,
Vec3::new(0.0, -0.5, 0.0),
Mat3::from_diagonal(&Vec3::new(0.1, 0.13, 0.11)),
)
}
fn chain(n: usize) -> Model {
let off = SpatialTransform::new(Mat3::identity(), Vec3::new(0.0, -1.0, 0.0));
let mut b = ModelBuilder::new()
.gravity(Vec3::new(0.0, -9.81, 0.0))
.dt(2.0e-3);
for i in 0..n {
let xf = if i == 0 {
SpatialTransform::identity()
} else {
off
};
b = b.add_revolute_body(
&format!("l{i}"),
i as i32 - 1,
xf,
link(1.0 + 0.1 * i as f64),
);
}
b.build()
}
#[test]
fn vector_mode_matches_scalar_dual_bitwise() {
for n in [1usize, 2, 3, 5, 8, 17, 20] {
let model = chain(n);
let layout = super::super::step::DofLayout::of(&model);
let q: Vec<f64> = (0..layout.nq).map(|i| 0.21 + 0.07 * i as f64).collect();
let v: Vec<f64> = (0..layout.nv).map(|i| -0.13 + 0.05 * i as f64).collect();
let ctrl: Vec<f64> = (0..layout.nv).map(|i| 0.02 * i as f64).collect();
let w: Vec<f64> = (0..layout.nv).map(|i| 1.0 - 0.03 * i as f64).collect();
for seed in [Seed::Position, Seed::Velocity] {
let inert: Vec<SpatialInertia<D>> = model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let fast = state_lanes(&model, &layout, None, &inert, &q, &v, &ctrl, &w, seed);
let slow = state_lanes_scalar(&model, &layout, &q, &v, &ctrl, &w, seed);
assert_eq!(fast.len(), slow.len(), "n = {n}, {seed:?}");
for (k, (a, b)) in fast.iter().zip(&slow).enumerate() {
assert_eq!(
a.to_bits(),
b.to_bits(),
"n = {n}, {seed:?}, lane {k}: {a} vs {b}"
);
}
}
}
}
#[test]
fn vector_mode_matches_scalar_dual_with_contact() {
let model = chain(3);
let layout = super::super::step::DofLayout::of(&model);
let meshes = [CollisionMesh {
body: 2,
vertices: vec![
Vec3::new(0.02, -1.0, 0.0),
Vec3::new(-0.02, -1.0, 0.01),
Vec3::new(0.0, -0.98, -0.02),
],
}];
let ground = GroundContact {
height: -1.5,
stiffness: 4.0e3,
damping: 40.0,
};
let contact = Some((&ground, &meshes[..]));
let q: Vec<f64> = (0..layout.nq).map(|i| 0.4 + 0.05 * i as f64).collect();
let v = vec![0.1, -0.2, 0.05];
let ctrl = vec![0.0; layout.nv];
let w = vec![0.9, 1.1, -0.7];
for seed in [Seed::Position, Seed::Velocity] {
let inert: Vec<SpatialInertia<D>> = model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let fast = state_lanes(&model, &layout, contact, &inert, &q, &v, &ctrl, &w, seed);
let lift = |xs: &[f64]| -> Vec<D> { xs.iter().map(|&x| D::constant(x)).collect() };
let (q_c, v_c, u_c) = (lift(&q), lift(&v), lift(&ctrl));
let inertias: Vec<SpatialInertia<D>> = model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let n = match seed {
Seed::Position => layout.nq,
Seed::Velocity => layout.nv,
};
for k in 0..n {
let (mut q_d, mut v_d) = (q_c.clone(), v_c.clone());
match seed {
Seed::Position => q_d[k] = D::var(q[k]),
Seed::Velocity => v_d[k] = D::var(v[k]),
}
let (_, _, qdd) =
step_generic(&model, &layout, &inertias, contact, None, &q_d, &v_d, &u_c);
let slow: f64 = qdd.iter().zip(&w).map(|(a, &wi)| a.dual * wi).sum();
assert_eq!(
fast[k].to_bits(),
slow.to_bits(),
"{seed:?} lane {k}: {} vs {slow}",
fast[k]
);
}
}
}
#[test]
fn lanes_are_not_trivially_zero() {
let model = chain(2);
let layout = super::super::step::DofLayout::of(&model);
let q = vec![0.3, 0.5];
let v = vec![0.2, -0.1];
let ctrl = vec![0.0, 0.0];
let w = vec![1.0, 1.0];
let inert: Vec<SpatialInertia<D>> = model
.bodies
.iter()
.map(|b| lift_inertia(&b.inertia))
.collect();
let aq = state_lanes(
&model,
&layout,
None,
&inert,
&q,
&v,
&ctrl,
&w,
Seed::Position,
);
assert!(
aq.iter().any(|x| x.abs() > 1e-9),
"every position lane vanished: {aq:?}"
);
}
}