1use phyz_math::SpatialTransform;
12use phyz_math::{DVec, Mat6, SpatialMat, SpatialVec, Vec3, Vec6};
13use phyz_model::{JointType, Model, State};
14
15fn joint_velocity(joint: &phyz_model::Joint, qd: &[f64]) -> SpatialVec {
17 match joint.joint_type {
18 JointType::Revolute | JointType::Hinge => {
19 SpatialVec::new(joint.axis * qd[0], Vec3::zeros())
20 }
21 JointType::Prismatic | JointType::Slide => {
22 SpatialVec::new(Vec3::zeros(), joint.axis * qd[0])
23 }
24 JointType::Spherical | JointType::Ball => {
25 SpatialVec::new(Vec3::new(qd[0], qd[1], qd[2]), Vec3::zeros())
26 }
27 JointType::Free => SpatialVec::new(
28 Vec3::new(qd[0], qd[1], qd[2]),
29 Vec3::new(qd[3], qd[4], qd[5]),
30 ),
31 JointType::Fixed => SpatialVec::zero(),
32 }
33}
34
35pub fn aba(model: &Model, state: &State) -> DVec {
40 aba_with_external_forces(model, state, None)
41}
42
43pub fn aba_with_external_forces(
45 model: &Model,
46 state: &State,
47 external_forces: Option<&[SpatialVec]>,
48) -> DVec {
49 let nb = model.nbodies();
50 let mut qdd = DVec::zeros(model.nv);
51
52 let mut x_tree = vec![SpatialTransform::identity(); nb];
54 let mut vel = vec![SpatialVec::zero(); nb];
55 let mut c_bias = vec![SpatialVec::zero(); nb];
56 let mut p_a = vec![SpatialVec::zero(); nb]; let mut i_a = vec![SpatialMat::zero(); nb]; let a0 = SpatialVec::new(Vec3::zeros(), -model.gravity);
61
62 for i in 0..nb {
64 let body = &model.bodies[i];
65 let joint = &model.joints[body.joint_idx];
66 let q_idx = model.q_offsets[body.joint_idx];
67 let v_idx = model.v_offsets[body.joint_idx];
68 let ndof = joint.ndof();
69
70 let x_joint = if ndof == 0 {
72 SpatialTransform::identity()
73 } else {
74 let q_slice = &state.q.as_slice()[q_idx..q_idx + ndof];
75 joint.joint_transform_slice(q_slice)
76 };
77 x_tree[i] = x_joint.compose(&joint.parent_to_joint);
78
79 let v_joint = joint_velocity(joint, &state.v.as_slice()[v_idx..v_idx + ndof]);
80
81 if body.parent < 0 {
82 vel[i] = v_joint;
83 c_bias[i] = SpatialVec::zero();
84 } else {
85 let pi = body.parent as usize;
86 let v_parent = x_tree[i].apply_motion(&vel[pi]);
87 vel[i] = v_parent + v_joint;
88 c_bias[i] = vel[i].cross_motion(&v_joint);
89 }
90
91 i_a[i] = body.inertia.to_matrix();
93 p_a[i] = vel[i].cross_force(&i_a[i].mul_vec(&vel[i]));
95
96 if let Some(ext) = external_forces {
98 p_a[i] = p_a[i] - ext[i];
99 }
100 }
101
102 for i in (0..nb).rev() {
104 let body = &model.bodies[i];
105 let joint = &model.joints[body.joint_idx];
106 let v_idx = model.v_offsets[body.joint_idx];
107 let ndof = joint.ndof();
108
109 if ndof == 0 {
110 if body.parent >= 0 {
112 let pi = body.parent as usize;
113 let x_mot = x_tree[i].to_motion_matrix();
114 let ia_parent = SpatialMat::from_mat6(x_mot.transpose() * i_a[i].data * x_mot);
115 let p_parent = x_tree[i].inv_apply_force(&p_a[i]);
116 i_a[pi] = i_a[pi] + ia_parent;
117 p_a[pi] = p_a[pi] + p_parent;
118 }
119 continue;
120 }
121
122 if ndof == 1 {
123 let s_i = joint.motion_subspace();
125 let phyz_i = state.ctrl[v_idx] - joint.damping * state.v[v_idx];
126
127 let ia = &i_a[i];
128 let u_i = phyz_i - s_i.dot(&p_a[i]);
129 let d_i = s_i.dot(&ia.mul_vec(&s_i));
130
131 if d_i.abs() < 1e-20 {
132 continue;
133 }
134
135 let u_inv_d = u_i / d_i;
136 let ia_s = ia.mul_vec(&s_i);
137
138 if body.parent >= 0 {
139 let pi = body.parent as usize;
140 let outer = outer_product_6(&ia_s, &ia.mul_vec(&s_i));
141 let ia_new = SpatialMat::from_mat6(ia.data - outer.data / d_i);
142 let p_new = p_a[i] + ia_new.mul_vec(&c_bias[i]) + ia_s * u_inv_d;
143
144 let x_mot = x_tree[i].to_motion_matrix();
145 let ia_parent = SpatialMat::from_mat6(x_mot.transpose() * ia_new.data * x_mot);
146 let p_parent = x_tree[i].inv_apply_force(&p_new);
147
148 i_a[pi] = i_a[pi] + ia_parent;
149 p_a[pi] = p_a[pi] + p_parent;
150 }
151 } else {
152 let s_mat = joint.motion_subspace_matrix(); let ia = &i_a[i];
155
156 let mut phyz_vec = nalgebra::DVector::<f64>::zeros(ndof);
158 for k in 0..ndof {
159 phyz_vec[k] = state.ctrl[v_idx + k] - joint.damping * state.v[v_idx + k];
160 }
161
162 let ia_mat = &ia.data;
164 let u_mat = ia_mat * &s_mat; let d_mat = s_mat.transpose() * &u_mat; let pa_vec = nalgebra::DVector::from_column_slice(p_a[i].data.as_slice());
171 let u_vec = &phyz_vec - &(s_mat.transpose() * &pa_vec); let d_inv = match nalgebra::DMatrix::from_iterator(
175 ndof,
176 ndof,
177 d_mat.iter().cloned(),
178 )
179 .try_inverse()
180 {
181 Some(inv) => inv,
182 None => continue,
183 };
184
185 if body.parent >= 0 {
186 let pi = body.parent as usize;
187
188 let u_dinv = &u_mat * &d_inv; let ia_new_data = ia_mat - &u_dinv * u_mat.transpose(); let ia_new = SpatialMat::from_mat6(Mat6::from_iterator(ia_new_data.iter().cloned()));
192
193 let dinv_u = &d_inv * &u_vec; let u_dinv_u = &u_mat * &dinv_u; let c_vec = c_bias[i].data;
197 let p_new_data = p_a[i].data + ia_new.data * c_vec + Vec6::from_iterator(u_dinv_u.iter().cloned());
198 let p_new = SpatialVec { data: p_new_data };
199
200 let x_mot = x_tree[i].to_motion_matrix();
201 let ia_parent = SpatialMat::from_mat6(x_mot.transpose() * ia_new.data * x_mot);
202 let p_parent = x_tree[i].inv_apply_force(&p_new);
203
204 i_a[pi] = i_a[pi] + ia_parent;
205 p_a[pi] = p_a[pi] + p_parent;
206 }
207 }
208 }
209
210 let mut acc = vec![SpatialVec::zero(); nb];
212
213 for i in 0..nb {
214 let body = &model.bodies[i];
215 let joint = &model.joints[body.joint_idx];
216 let v_idx = model.v_offsets[body.joint_idx];
217 let ndof = joint.ndof();
218
219 let a_parent = if body.parent < 0 {
220 x_tree[i].apply_motion(&a0)
221 } else {
222 let pi = body.parent as usize;
223 x_tree[i].apply_motion(&acc[pi])
224 };
225
226 if ndof == 0 {
227 acc[i] = a_parent + c_bias[i];
228 continue;
229 }
230
231 if ndof == 1 {
232 let s_i = joint.motion_subspace();
233 let ia = &i_a[i];
234 let d_i = s_i.dot(&ia.mul_vec(&s_i));
235
236 if d_i.abs() < 1e-20 {
237 acc[i] = a_parent + c_bias[i];
238 continue;
239 }
240
241 let phyz_i = state.ctrl[v_idx] - joint.damping * state.v[v_idx];
242 let u_i = phyz_i - s_i.dot(&p_a[i]);
243 let qdd_i = (u_i - ia.mul_vec(&(a_parent + c_bias[i])).dot(&s_i)) / d_i;
244 qdd[v_idx] = qdd_i;
245 acc[i] = a_parent + c_bias[i] + s_i * qdd_i;
246 } else {
247 let s_mat = joint.motion_subspace_matrix();
248 let ia = &i_a[i];
249 let ia_mat = &ia.data;
250
251 let mut phyz_vec = nalgebra::DVector::<f64>::zeros(ndof);
252 for k in 0..ndof {
253 phyz_vec[k] = state.ctrl[v_idx + k] - joint.damping * state.v[v_idx + k];
254 }
255
256 let u_mat = ia_mat * &s_mat;
257 let d_mat = s_mat.transpose() * &u_mat;
258
259 let d_inv = match nalgebra::DMatrix::from_iterator(
260 ndof,
261 ndof,
262 d_mat.iter().cloned(),
263 )
264 .try_inverse()
265 {
266 Some(inv) => inv,
267 None => {
268 acc[i] = a_parent + c_bias[i];
269 continue;
270 }
271 };
272
273 let pa_vec = nalgebra::DVector::from_column_slice(p_a[i].data.as_slice());
274 let u_vec = &phyz_vec - &(s_mat.transpose() * &pa_vec);
275
276 let a_total = a_parent + c_bias[i];
277 let a_vec = nalgebra::DVector::from_column_slice(a_total.data.as_slice());
278 let st_ia_a = s_mat.transpose() * (ia_mat * &a_vec);
279
280 let qdd_vec = &d_inv * (&u_vec - &st_ia_a);
281
282 for k in 0..ndof {
283 qdd[v_idx + k] = qdd_vec[k];
284 }
285
286 let s_qdd = &s_mat * &qdd_vec;
288 acc[i] = a_total + SpatialVec {
289 data: Vec6::from_iterator(s_qdd.iter().cloned()),
290 };
291 }
292 }
293
294 qdd
295}
296
297fn outer_product_6(a: &SpatialVec, b: &SpatialVec) -> SpatialMat {
299 SpatialMat::from_mat6(a.data * b.data.transpose())
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use phyz_math::{GRAVITY, SpatialInertia};
306 use phyz_model::ModelBuilder;
307
308 fn make_double_pendulum() -> phyz_model::Model {
309 let length = 1.0;
310 let mass = 1.0;
311 ModelBuilder::new()
312 .gravity(Vec3::new(0.0, -GRAVITY, 0.0))
313 .dt(0.001)
314 .add_revolute_body(
315 "link1",
316 -1,
317 SpatialTransform::identity(),
318 SpatialInertia::new(
319 mass,
320 Vec3::new(0.0, -length / 2.0, 0.0),
321 phyz_math::Mat3::from_diagonal(&Vec3::new(
322 mass * length * length / 12.0,
323 0.0,
324 mass * length * length / 12.0,
325 )),
326 ),
327 )
328 .add_revolute_body(
329 "link2",
330 0,
331 SpatialTransform::translation(Vec3::new(0.0, -length, 0.0)),
332 SpatialInertia::new(
333 mass,
334 Vec3::new(0.0, -length / 2.0, 0.0),
335 phyz_math::Mat3::from_diagonal(&Vec3::new(
336 mass * length * length / 12.0,
337 0.0,
338 mass * length * length / 12.0,
339 )),
340 ),
341 )
342 .build()
343 }
344
345 #[test]
346 fn test_double_pendulum_equilibrium() {
347 let model = make_double_pendulum();
348 let state = model.default_state();
349 let qdd = aba(&model, &state);
350 assert!(qdd[0].abs() < 1e-10, "qdd[0] = {} at equilibrium", qdd[0]);
351 assert!(qdd[1].abs() < 1e-10, "qdd[1] = {} at equilibrium", qdd[1]);
352 }
353
354 #[test]
355 fn test_aba_rnea_consistency() {
356 let model = make_double_pendulum();
357 let mut state = model.default_state();
358 state.q[0] = 0.3;
359 state.q[1] = 0.2;
360 state.v[0] = 0.1;
361 state.v[1] = -0.1;
362
363 let qdd = aba(&model, &state);
364 let tau = crate::rnea(&model, &state, &qdd);
365
366 assert!(tau[0].abs() < 1e-10, "RNEA tau[0] = {}", tau[0]);
368 assert!(tau[1].abs() < 1e-10, "RNEA tau[1] = {}", tau[1]);
369 }
370
371 #[test]
372 fn test_single_pendulum_aba() {
373 let length = 1.0;
374 let mass = 1.0;
375
376 let model = ModelBuilder::new()
377 .gravity(Vec3::new(0.0, -GRAVITY, 0.0))
378 .add_revolute_body(
379 "link1",
380 -1,
381 SpatialTransform::identity(),
382 SpatialInertia::new(
383 mass,
384 Vec3::new(0.0, -length / 2.0, 0.0),
385 phyz_math::Mat3::from_diagonal(&Vec3::new(
386 mass * length * length / 12.0,
387 0.0,
388 mass * length * length / 12.0,
389 )),
390 ),
391 )
392 .build();
393
394 let mut state = model.default_state();
395 state.q[0] = std::f64::consts::FRAC_PI_2;
396
397 let qdd = aba(&model, &state);
398
399 let i_total = mass * length * length / 3.0;
400 let expected_qdd = -(mass * GRAVITY * length / 2.0) / i_total;
401
402 assert!(
403 (qdd[0] - expected_qdd).abs() < 1e-6,
404 "qdd = {}, expected = {}",
405 qdd[0],
406 expected_qdd
407 );
408 }
409
410 #[test]
411 fn test_free_joint_freefall() {
412 let model = ModelBuilder::new()
414 .gravity(Vec3::new(0.0, 0.0, -GRAVITY))
415 .add_free_body(
416 "ball",
417 -1,
418 SpatialTransform::identity(),
419 SpatialInertia::sphere(1.0, 0.1),
420 )
421 .build();
422
423 let state = model.default_state();
424 let qdd = aba(&model, &state);
425
426 assert!(qdd[0].abs() < 1e-10, "ang_x accel = {}", qdd[0]);
429 assert!(qdd[1].abs() < 1e-10, "ang_y accel = {}", qdd[1]);
430 assert!(qdd[2].abs() < 1e-10, "ang_z accel = {}", qdd[2]);
431 assert!(qdd[3].abs() < 1e-10, "lin_x accel = {}", qdd[3]);
432 assert!(qdd[4].abs() < 1e-10, "lin_y accel = {}", qdd[4]);
433 assert!(
434 (qdd[5] - (-GRAVITY)).abs() < 1e-6,
435 "lin_z accel = {}, expected = {}",
436 qdd[5],
437 -GRAVITY
438 );
439 }
440}