1use phyz_model::{JointType, Model};
29use tang::Scalar;
30use tang::{Mat3, SpatialInertia, SpatialMat, SpatialTransform, SpatialVec, Vec3};
31use tang_expr::{ExprId, trace};
32use tang_la::DMat;
33
34use crate::StepJacobians;
35
36type CompiledFn = Box<dyn Fn(&[f64], &mut [f64])>;
38
39pub struct CompiledStepJacobians {
44 nq: usize,
46 nv: usize,
48 num_inputs: usize,
50 jac_fn: CompiledFn,
53 num_outputs: usize,
55}
56
57impl CompiledStepJacobians {
58 pub fn build(model: &Model) -> Self {
66 let nq = model.nq;
67 let nv = model.nv;
68 let num_inputs = nq + nv + nv; let (mut graph, (qnext_exprs, vnext_exprs)) = trace(|| symbolic_step(model));
72
73 let num_jac_entries = nq * nq + nq * nv + nv * nq + nv * nv + nv * nv;
75 let mut jac_exprs = Vec::with_capacity(num_jac_entries);
76
77 for &qn in &qnext_exprs {
79 for j in 0..nq {
80 let d = graph.diff(qn, j as u16);
81 let d = graph.simplify(d);
82 jac_exprs.push(d);
83 }
84 }
85
86 for &qn in &qnext_exprs {
88 for j in 0..nv {
89 let var_idx = (nq + j) as u16;
90 let d = graph.diff(qn, var_idx);
91 let d = graph.simplify(d);
92 jac_exprs.push(d);
93 }
94 }
95
96 for &vn in &vnext_exprs {
98 for j in 0..nq {
99 let d = graph.diff(vn, j as u16);
100 let d = graph.simplify(d);
101 jac_exprs.push(d);
102 }
103 }
104
105 for &vn in &vnext_exprs {
107 for j in 0..nv {
108 let var_idx = (nq + j) as u16;
109 let d = graph.diff(vn, var_idx);
110 let d = graph.simplify(d);
111 jac_exprs.push(d);
112 }
113 }
114
115 for &vn in &vnext_exprs {
117 for j in 0..nv {
118 let var_idx = (nq + nv + j) as u16;
119 let d = graph.diff(vn, var_idx);
120 let d = graph.simplify(d);
121 jac_exprs.push(d);
122 }
123 }
124
125 let jac_fn = graph.compile_many(&jac_exprs);
126
127 Self {
128 nq,
129 nv,
130 num_inputs,
131 jac_fn,
132 num_outputs: num_jac_entries,
133 }
134 }
135
136 pub fn eval(&self, inputs: &[f64]) -> StepJacobians {
140 assert_eq!(inputs.len(), self.num_inputs);
141
142 let mut flat = vec![0.0; self.num_outputs];
143 (self.jac_fn)(inputs, &mut flat);
144
145 let nq = self.nq;
146 let nv = self.nv;
147 let mut offset = 0;
148
149 let dqnext_dq = DMat::from_fn(nq, nq, |i, j| flat[offset + i * nq + j]);
151 offset += nq * nq;
152
153 let dqnext_dv = DMat::from_fn(nq, nv, |i, j| flat[offset + i * nv + j]);
155 offset += nq * nv;
156
157 let dvnext_dq = DMat::from_fn(nv, nq, |i, j| flat[offset + i * nq + j]);
159 offset += nv * nq;
160
161 let dvnext_dv = DMat::from_fn(nv, nv, |i, j| flat[offset + i * nv + j]);
163 offset += nv * nv;
164
165 let dvnext_dctrl = DMat::from_fn(nv, nv, |i, j| flat[offset + i * nv + j]);
167
168 StepJacobians {
169 dqnext_dq,
170 dqnext_dv,
171 dvnext_dq,
172 dvnext_dv,
173 dvnext_dctrl,
174 }
175 }
176}
177
178pub fn symbolic_step_jacobians(model: &Model, state: &phyz_model::State) -> StepJacobians {
184 let compiled = CompiledStepJacobians::build(model);
185 let mut inputs = Vec::with_capacity(model.nq + model.nv + model.nv);
186 inputs.extend_from_slice(state.q.as_slice());
187 inputs.extend_from_slice(state.v.as_slice());
188 inputs.extend_from_slice(state.ctrl.as_slice());
189 compiled.eval(&inputs)
190}
191
192fn symbolic_step(model: &Model) -> (Vec<ExprId>, Vec<ExprId>) {
205 let nq = model.nq;
206 let nv = model.nv;
207
208 let q: Vec<ExprId> = (0..nq).map(|i| ExprId::var(i as u16)).collect();
210 let v: Vec<ExprId> = (0..nv).map(|i| ExprId::var((nq + i) as u16)).collect();
211 let ctrl: Vec<ExprId> = (0..nv).map(|i| ExprId::var((nq + nv + i) as u16)).collect();
212
213 let qdd = symbolic_aba(model, &q, &v, &ctrl);
215
216 let dt = ExprId::from_f64(model.dt);
220
221 let v_next: Vec<ExprId> = (0..nv).map(|i| v[i] + dt * qdd[i]).collect();
222
223 let q_next: Vec<ExprId> = (0..nq).map(|i| q[i] + dt * v_next[i]).collect();
224
225 (q_next, v_next)
226}
227
228fn symbolic_aba(model: &Model, q: &[ExprId], v: &[ExprId], ctrl: &[ExprId]) -> Vec<ExprId> {
232 let nb = model.nbodies();
233 let nv = model.nv;
234 let zero_sv: SpatialVec<ExprId> = SpatialVec::zero();
235
236 let mut x_tree: Vec<SpatialTransform<ExprId>> = vec![SpatialTransform::identity(); nb];
238 let mut vel: Vec<SpatialVec<ExprId>> = vec![zero_sv; nb];
239 let mut c_bias: Vec<SpatialVec<ExprId>> = vec![zero_sv; nb];
240 let mut p_a: Vec<SpatialVec<ExprId>> = vec![zero_sv; nb];
241 let mut i_a: Vec<SpatialMat<ExprId>> = vec![SpatialMat::zero(); nb];
242
243 let grav = lift_vec3(model.gravity);
245 let a0: SpatialVec<ExprId> = SpatialVec::new(Vec3::zero(), -grav);
246
247 for i in 0..nb {
249 let body = &model.bodies[i];
250 let joint = &model.joints[body.joint_idx];
251 let q_idx = model.q_offsets[body.joint_idx];
252 let v_idx = model.v_offsets[body.joint_idx];
253 let ndof = joint.ndof();
254
255 assert!(
256 ndof <= 1,
257 "symbolic ABA only supports single-DOF joints (revolute/prismatic/fixed), \
258 found joint with ndof={ndof}"
259 );
260
261 let x_joint = if ndof == 0 {
263 SpatialTransform::identity()
264 } else {
265 sym_joint_transform(joint.joint_type, &lift_vec3(joint.axis), q[q_idx])
266 };
267
268 let parent_xf = lift_spatial_transform(&joint.parent_to_joint);
269 x_tree[i] = x_joint.compose(&parent_xf);
270
271 let v_joint = if ndof == 0 {
272 zero_sv
273 } else {
274 sym_joint_velocity(joint.joint_type, &lift_vec3(joint.axis), v[v_idx])
275 };
276
277 if body.parent < 0 {
278 vel[i] = v_joint;
279 c_bias[i] = zero_sv;
280 } else {
281 let pi = body.parent as usize;
282 let v_parent = x_tree[i].apply_motion(&vel[pi]);
283 vel[i] = v_parent + v_joint;
284 c_bias[i] = vel[i].cross_motion(&v_joint);
285 }
286
287 let inertia = lift_spatial_inertia(&body.inertia);
289 i_a[i] = inertia.to_matrix();
290
291 p_a[i] = vel[i].cross_force(&i_a[i].mul_vec(&vel[i]));
293 }
294
295 for i in (0..nb).rev() {
297 let body = &model.bodies[i];
298 let joint = &model.joints[body.joint_idx];
299 let v_idx = model.v_offsets[body.joint_idx];
300 let ndof = joint.ndof();
301
302 if ndof == 0 {
303 if body.parent >= 0 {
305 let pi = body.parent as usize;
306 let x_mot = x_tree[i].to_motion_matrix();
307 let ia_parent = x_mot.transpose().mul_mat(&i_a[i]).mul_mat(&x_mot);
308 let p_parent = x_tree[i].inv_apply_force(&p_a[i]);
309 i_a[pi] = i_a[pi] + ia_parent;
310 p_a[pi] = p_a[pi] + p_parent;
311 }
312 continue;
313 }
314
315 let s_i = sym_motion_subspace(joint.joint_type, &lift_vec3(joint.axis));
317
318 let damping_lit = ExprId::from_f64(joint.damping);
319 let tau_i = ctrl[v_idx] - damping_lit * v[v_idx];
320
321 let ia = &i_a[i];
322 let u_i = tau_i - s_i.dot(&p_a[i]);
323 let d_i = s_i.dot(&ia.mul_vec(&s_i));
324
325 let d_inv = d_i.recip();
326 let u_inv_d = u_i * d_inv;
327 let ia_s = ia.mul_vec(&s_i);
328
329 if body.parent >= 0 {
330 let pi = body.parent as usize;
331 let outer = sym_outer_product_6(&ia_s, &ia.mul_vec(&s_i));
332 let ia_new = *ia - outer * d_inv;
333 let p_new = p_a[i] + ia_new.mul_vec(&c_bias[i]) + ia_s * u_inv_d;
334
335 let x_mot = x_tree[i].to_motion_matrix();
336 let ia_parent = x_mot.transpose().mul_mat(&ia_new).mul_mat(&x_mot);
337 let p_parent = x_tree[i].inv_apply_force(&p_new);
338
339 i_a[pi] = i_a[pi] + ia_parent;
340 p_a[pi] = p_a[pi] + p_parent;
341 }
342 }
343
344 let mut acc: Vec<SpatialVec<ExprId>> = vec![zero_sv; nb];
346 let mut qdd = vec![ExprId::ZERO; nv];
347
348 for i in 0..nb {
349 let body = &model.bodies[i];
350 let joint = &model.joints[body.joint_idx];
351 let v_idx = model.v_offsets[body.joint_idx];
352 let ndof = joint.ndof();
353
354 let a_parent = if body.parent < 0 {
355 x_tree[i].apply_motion(&a0)
356 } else {
357 let pi = body.parent as usize;
358 x_tree[i].apply_motion(&acc[pi])
359 };
360
361 if ndof == 0 {
362 acc[i] = a_parent + c_bias[i];
363 continue;
364 }
365
366 let s_i = sym_motion_subspace(joint.joint_type, &lift_vec3(joint.axis));
367 let ia = &i_a[i];
368 let d_i = s_i.dot(&ia.mul_vec(&s_i));
369 let d_inv = d_i.recip();
370
371 let damping_lit = ExprId::from_f64(joint.damping);
372 let tau_i = ctrl[v_idx] - damping_lit * v[v_idx];
373 let u_i = tau_i - s_i.dot(&p_a[i]);
374
375 let qdd_i = (u_i - ia.mul_vec(&(a_parent + c_bias[i])).dot(&s_i)) * d_inv;
376 qdd[v_idx] = qdd_i;
377 acc[i] = a_parent + c_bias[i] + s_i * qdd_i;
378 }
379
380 qdd
381}
382
383fn lift_vec3(v: Vec3<f64>) -> Vec3<ExprId> {
389 Vec3::new(
390 ExprId::from_f64(v.x),
391 ExprId::from_f64(v.y),
392 ExprId::from_f64(v.z),
393 )
394}
395
396fn lift_mat3(m: Mat3<f64>) -> Mat3<ExprId> {
398 Mat3::new(
399 ExprId::from_f64(m.c0.x),
400 ExprId::from_f64(m.c1.x),
401 ExprId::from_f64(m.c2.x),
402 ExprId::from_f64(m.c0.y),
403 ExprId::from_f64(m.c1.y),
404 ExprId::from_f64(m.c2.y),
405 ExprId::from_f64(m.c0.z),
406 ExprId::from_f64(m.c1.z),
407 ExprId::from_f64(m.c2.z),
408 )
409}
410
411fn lift_spatial_transform(xf: &tang::SpatialTransform<f64>) -> SpatialTransform<ExprId> {
413 SpatialTransform::new(lift_mat3(xf.rot), lift_vec3(xf.pos))
414}
415
416fn lift_spatial_inertia(si: &tang::SpatialInertia<f64>) -> SpatialInertia<ExprId> {
418 SpatialInertia::new(
419 ExprId::from_f64(si.mass),
420 lift_vec3(si.com),
421 lift_mat3(si.inertia),
422 )
423}
424
425fn sym_joint_transform(
431 joint_type: JointType,
432 axis: &Vec3<ExprId>,
433 q: ExprId,
434) -> SpatialTransform<ExprId> {
435 match joint_type {
436 JointType::Revolute | JointType::Hinge => {
437 let (s, c) = (-q).sin_cos();
439 let ax = tang::skew(axis);
440 let rot = Mat3::identity() + ax * s + ax.mul_mat(&ax) * (ExprId::ONE - c);
441 SpatialTransform::new(rot, Vec3::zero())
442 }
443 JointType::Prismatic | JointType::Slide => {
444 let pos = *axis * q;
445 SpatialTransform::new(Mat3::identity(), pos)
446 }
447 JointType::Fixed => SpatialTransform::identity(),
448 _ => panic!(
449 "symbolic joint transform not supported for {:?}",
450 joint_type
451 ),
452 }
453}
454
455fn sym_joint_velocity(
457 joint_type: JointType,
458 axis: &Vec3<ExprId>,
459 qd: ExprId,
460) -> SpatialVec<ExprId> {
461 match joint_type {
462 JointType::Revolute | JointType::Hinge => SpatialVec::new(*axis * qd, Vec3::zero()),
463 JointType::Prismatic | JointType::Slide => SpatialVec::new(Vec3::zero(), *axis * qd),
464 JointType::Fixed => SpatialVec::zero(),
465 _ => panic!("symbolic joint velocity not supported for {:?}", joint_type),
466 }
467}
468
469fn sym_motion_subspace(joint_type: JointType, axis: &Vec3<ExprId>) -> SpatialVec<ExprId> {
471 match joint_type {
472 JointType::Revolute | JointType::Hinge => SpatialVec::new(*axis, Vec3::zero()),
473 JointType::Prismatic | JointType::Slide => SpatialVec::new(Vec3::zero(), *axis),
474 _ => panic!("motion_subspace not supported for {:?}", joint_type),
475 }
476}
477
478fn sym_outer_product_6(a: &SpatialVec<ExprId>, b: &SpatialVec<ExprId>) -> SpatialMat<ExprId> {
480 fn vec3_outer(a: Vec3<ExprId>, b: Vec3<ExprId>) -> Mat3<ExprId> {
481 Mat3::new(
482 a.x * b.x,
483 a.x * b.y,
484 a.x * b.z,
485 a.y * b.x,
486 a.y * b.y,
487 a.y * b.z,
488 a.z * b.x,
489 a.z * b.y,
490 a.z * b.z,
491 )
492 }
493
494 SpatialMat::new(
495 vec3_outer(a.angular, b.angular),
496 vec3_outer(a.angular, b.linear),
497 vec3_outer(a.linear, b.angular),
498 vec3_outer(a.linear, b.linear),
499 )
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use phyz_math::{GRAVITY, SpatialInertia as ConcreteSI, SpatialTransform as ConcreteXF};
506 use phyz_model::ModelBuilder;
507
508 fn make_pendulum() -> Model {
509 ModelBuilder::new()
510 .gravity(phyz_math::Vec3::new(0.0, -GRAVITY, 0.0))
511 .dt(0.001)
512 .add_revolute_body(
513 "link1",
514 -1,
515 ConcreteXF::identity(),
516 ConcreteSI::new(
517 1.0,
518 phyz_math::Vec3::new(0.0, -0.5, 0.0),
519 phyz_math::Mat3::from_diagonal(&phyz_math::Vec3::new(
520 1.0 / 12.0,
521 0.0,
522 1.0 / 12.0,
523 )),
524 ),
525 )
526 .build()
527 }
528
529 fn make_double_pendulum() -> Model {
530 let length = 1.0;
531 let mass = 1.0;
532 ModelBuilder::new()
533 .gravity(phyz_math::Vec3::new(0.0, -GRAVITY, 0.0))
534 .dt(0.001)
535 .add_revolute_body(
536 "link1",
537 -1,
538 ConcreteXF::identity(),
539 ConcreteSI::new(
540 mass,
541 phyz_math::Vec3::new(0.0, -length / 2.0, 0.0),
542 phyz_math::Mat3::from_diagonal(&phyz_math::Vec3::new(
543 mass * length * length / 12.0,
544 0.0,
545 mass * length * length / 12.0,
546 )),
547 ),
548 )
549 .add_revolute_body(
550 "link2",
551 0,
552 ConcreteXF::from_translation(phyz_math::Vec3::new(0.0, -length, 0.0)),
553 ConcreteSI::new(
554 mass,
555 phyz_math::Vec3::new(0.0, -length / 2.0, 0.0),
556 phyz_math::Mat3::from_diagonal(&phyz_math::Vec3::new(
557 mass * length * length / 12.0,
558 0.0,
559 mass * length * length / 12.0,
560 )),
561 ),
562 )
563 .build()
564 }
565
566 #[test]
567 fn test_symbolic_vs_fd_pendulum() {
568 let model = make_pendulum();
569 let mut state = model.default_state();
570 state.q[0] = 0.3;
571 state.v[0] = 0.1;
572
573 let sym = symbolic_step_jacobians(&model, &state);
574 let fd = crate::finite_diff_jacobians(&model, &state, 1e-6);
575
576 let eps = 1e-4;
577 assert!(
578 (&fd.dqnext_dq - &sym.dqnext_dq).norm() < eps,
579 "dqnext_dq mismatch: fd={:?}, sym={:?}",
580 fd.dqnext_dq,
581 sym.dqnext_dq
582 );
583 assert!(
584 (&fd.dqnext_dv - &sym.dqnext_dv).norm() < eps,
585 "dqnext_dv mismatch: fd={:?}, sym={:?}",
586 fd.dqnext_dv,
587 sym.dqnext_dv
588 );
589 assert!(
590 (&fd.dvnext_dq - &sym.dvnext_dq).norm() < eps,
591 "dvnext_dq mismatch: fd={:?}, sym={:?}",
592 fd.dvnext_dq,
593 sym.dvnext_dq
594 );
595 assert!(
596 (&fd.dvnext_dv - &sym.dvnext_dv).norm() < eps,
597 "dvnext_dv mismatch: fd={:?}, sym={:?}",
598 fd.dvnext_dv,
599 sym.dvnext_dv
600 );
601 assert!(
602 (&fd.dvnext_dctrl - &sym.dvnext_dctrl).norm() < eps,
603 "dvnext_dctrl mismatch: fd={:?}, sym={:?}",
604 fd.dvnext_dctrl,
605 sym.dvnext_dctrl
606 );
607 }
608
609 #[test]
610 fn test_symbolic_vs_fd_double_pendulum() {
611 let model = make_double_pendulum();
612 let mut state = model.default_state();
613 state.q[0] = 0.3;
614 state.q[1] = -0.2;
615 state.v[0] = 0.1;
616 state.v[1] = -0.05;
617
618 let sym = symbolic_step_jacobians(&model, &state);
619 let fd = crate::finite_diff_jacobians(&model, &state, 1e-6);
620
621 let eps = 1e-4;
622 assert!(
623 (&fd.dqnext_dq - &sym.dqnext_dq).norm() < eps,
624 "dqnext_dq mismatch: norm={}",
625 (&fd.dqnext_dq - &sym.dqnext_dq).norm()
626 );
627 assert!(
628 (&fd.dqnext_dv - &sym.dqnext_dv).norm() < eps,
629 "dqnext_dv mismatch: norm={}",
630 (&fd.dqnext_dv - &sym.dqnext_dv).norm()
631 );
632 assert!(
633 (&fd.dvnext_dq - &sym.dvnext_dq).norm() < eps,
634 "dvnext_dq mismatch: norm={}",
635 (&fd.dvnext_dq - &sym.dvnext_dq).norm()
636 );
637 assert!(
638 (&fd.dvnext_dv - &sym.dvnext_dv).norm() < eps,
639 "dvnext_dv mismatch: norm={}",
640 (&fd.dvnext_dv - &sym.dvnext_dv).norm()
641 );
642 assert!(
643 (&fd.dvnext_dctrl - &sym.dvnext_dctrl).norm() < eps,
644 "dvnext_dctrl mismatch: norm={}",
645 (&fd.dvnext_dctrl - &sym.dvnext_dctrl).norm()
646 );
647 }
648
649 #[test]
650 fn test_compiled_reuse() {
651 let model = make_pendulum();
652
653 let compiled = CompiledStepJacobians::build(&model);
655
656 let inputs1 = vec![0.3, 0.1, 0.0]; let jac1 = compiled.eval(&inputs1);
659
660 let inputs2 = vec![0.5, -0.2, 1.0];
662 let jac2 = compiled.eval(&inputs2);
663
664 assert!(
666 (&jac1.dvnext_dq - &jac2.dvnext_dq).norm() > 1e-6,
667 "Jacobians should differ at different states"
668 );
669 }
670
671 #[test]
672 fn test_symbolic_vs_analytical() {
673 let model = make_pendulum();
675 let mut state = model.default_state();
676 state.q[0] = 0.3;
677 state.v[0] = 0.1;
678
679 let sym = symbolic_step_jacobians(&model, &state);
680 let anal = crate::semi_implicit_step_jacobians(&model, &state);
681
682 let eps = 1e-4;
683 assert!(
684 (&anal.dqnext_dq - &sym.dqnext_dq).norm() < eps,
685 "dqnext_dq: analytical vs symbolic mismatch"
686 );
687 assert!(
688 (&anal.dvnext_dq - &sym.dvnext_dq).norm() < eps,
689 "dvnext_dq: analytical vs symbolic mismatch"
690 );
691 assert!(
692 (&anal.dvnext_dv - &sym.dvnext_dv).norm() < eps,
693 "dvnext_dv: analytical vs symbolic mismatch"
694 );
695 assert!(
696 (&anal.dvnext_dctrl - &sym.dvnext_dctrl).norm() < eps,
697 "dvnext_dctrl: analytical vs symbolic mismatch"
698 );
699 }
700}