1#![warn(missing_docs)]
47
48#[cfg(doctest)]
51#[doc = include_str!("../README.md")]
52pub struct ReadmeDocTests;
53
54pub mod contact_adjoint;
55pub mod multidual;
56pub mod rollout;
57pub mod symbolic;
58
59pub use rollout::{
63 AdjointGradients, AdjointRollout, CollisionMesh, ContactSetup, FinalStateObjective,
64 GroundContact, N_INERTIA_PARAMS, adjoint_rollout_gradient, inertia_params, rollout_objective,
65};
66
67pub use contact_adjoint::{
70 ConvexAdjointError, ConvexAdjointGradients, ConvexContactRollout, convex_adjoint_gradient,
71 convex_rollout_objective,
72};
73
74use phyz_math::{DMat, DVec};
75use phyz_model::{Model, State};
76use phyz_rigid::aba;
77
78#[derive(Debug, Clone)]
87pub struct StepJacobians {
88 pub dqnext_dq: DMat,
90 pub dqnext_dv: DMat,
92 pub dvnext_dq: DMat,
94 pub dvnext_dv: DMat,
96 pub dvnext_dctrl: DMat,
98}
99
100fn set_col(m: &mut DMat, j: usize, col: &DVec) {
102 for i in 0..col.len() {
103 m[(i, j)] = col[i];
104 }
105}
106
107pub fn finite_diff_jacobians(model: &Model, state: &State, eps: f64) -> StepJacobians {
112 let nq = model.nq;
113 let nv = model.nv;
114 let dt = model.dt;
115
116 let (_q_nom, _v_nom) = semi_implicit_euler_step(model, state, dt);
118
119 let mut dqnext_dq = DMat::zeros(nq, nq);
120 let mut dqnext_dv = DMat::zeros(nq, nv);
121 let mut dvnext_dq = DMat::zeros(nv, nq);
122 let mut dvnext_dv = DMat::zeros(nv, nv);
123 let mut dvnext_dctrl = DMat::zeros(nv, nv);
124
125 let inv_2eps = 1.0 / (2.0 * eps);
126
127 for j in 0..nq {
129 let mut s_plus = state.clone();
130 s_plus.q[j] += eps;
131 let (qp, vp) = semi_implicit_euler_step(model, &s_plus, dt);
132
133 let mut s_minus = state.clone();
134 s_minus.q[j] -= eps;
135 let (qm, vm) = semi_implicit_euler_step(model, &s_minus, dt);
136
137 set_col(&mut dqnext_dq, j, &(&(&qp - &qm) * inv_2eps));
138 set_col(&mut dvnext_dq, j, &(&(&vp - &vm) * inv_2eps));
139 }
140
141 for j in 0..nv {
143 let mut s_plus = state.clone();
144 s_plus.v[j] += eps;
145 let (qp, vp) = semi_implicit_euler_step(model, &s_plus, dt);
146
147 let mut s_minus = state.clone();
148 s_minus.v[j] -= eps;
149 let (qm, vm) = semi_implicit_euler_step(model, &s_minus, dt);
150
151 set_col(&mut dqnext_dv, j, &(&(&qp - &qm) * inv_2eps));
152 set_col(&mut dvnext_dv, j, &(&(&vp - &vm) * inv_2eps));
153 }
154
155 for j in 0..nv {
157 let mut s_plus = state.clone();
158 s_plus.ctrl[j] += eps;
159 let (_, vp) = semi_implicit_euler_step(model, &s_plus, dt);
160
161 let mut s_minus = state.clone();
162 s_minus.ctrl[j] -= eps;
163 let (_, vm) = semi_implicit_euler_step(model, &s_minus, dt);
164
165 set_col(&mut dvnext_dctrl, j, &(&(&vp - &vm) * inv_2eps));
166 }
167
168 StepJacobians {
169 dqnext_dq,
170 dqnext_dv,
171 dvnext_dq,
172 dvnext_dv,
173 dvnext_dctrl,
174 }
175}
176
177#[deprecated(
182 since = "0.1.0",
183 note = "this is not analytical — the ABA block uses finite differences; use `semi_implicit_step_jacobians`, or `symbolic::symbolic_step_jacobians` for exact derivatives"
184)]
185pub fn analytical_step_jacobians(model: &Model, state: &State) -> StepJacobians {
186 semi_implicit_step_jacobians(model, state)
187}
188
189pub fn semi_implicit_step_jacobians(model: &Model, state: &State) -> StepJacobians {
205 let nq = model.nq;
206 let nv = model.nv;
207 let dt = model.dt;
208 let eps = 1e-7;
209 let inv_2eps = 1.0 / (2.0 * eps);
210
211 let _qdd_nom = aba(model, state);
213
214 let mut dqdd_dq = DMat::zeros(nv, nq);
215 let mut dqdd_dv = DMat::zeros(nv, nv);
216 let mut dqdd_dctrl = DMat::zeros(nv, nv);
217
218 for j in 0..nq {
219 let mut sp = state.clone();
220 sp.q[j] += eps;
221 let qddp = aba(model, &sp);
222
223 let mut sm = state.clone();
224 sm.q[j] -= eps;
225 let qddm = aba(model, &sm);
226
227 set_col(&mut dqdd_dq, j, &(&(&qddp - &qddm) * inv_2eps));
228 }
229
230 for j in 0..nv {
231 let mut sp = state.clone();
232 sp.v[j] += eps;
233 let qddp = aba(model, &sp);
234
235 let mut sm = state.clone();
236 sm.v[j] -= eps;
237 let qddm = aba(model, &sm);
238
239 set_col(&mut dqdd_dv, j, &(&(&qddp - &qddm) * inv_2eps));
240 }
241
242 for j in 0..nv {
243 let mut sp = state.clone();
244 sp.ctrl[j] += eps;
245 let qddp = aba(model, &sp);
246
247 let mut sm = state.clone();
248 sm.ctrl[j] -= eps;
249 let qddm = aba(model, &sm);
250
251 set_col(&mut dqdd_dctrl, j, &(&(&qddp - &qddm) * inv_2eps));
252 }
253
254 let i_nv = DMat::identity(nv);
265 let i_nq = DMat::identity(nq);
266
267 let dvnext_dq = dqdd_dq.scale(dt);
268 let dvnext_dv = &i_nv + &dqdd_dv.scale(dt);
269 let dvnext_dctrl = dqdd_dctrl.scale(dt);
270 let dqnext_dq = &i_nq + &dvnext_dq.scale(dt);
271 let dqnext_dv = dvnext_dv.scale(dt);
272
273 StepJacobians {
274 dqnext_dq,
275 dqnext_dv,
276 dvnext_dq,
277 dvnext_dv,
278 dvnext_dctrl,
279 }
280}
281
282fn semi_implicit_euler_step(model: &Model, state: &State, dt: f64) -> (DVec, DVec) {
284 let qdd = aba(model, state);
285 let v_new = &state.v + &(&qdd * dt);
286 let q_new = &state.q + &(&v_new * dt);
287 (q_new, v_new)
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use phyz_math::{GRAVITY, Mat3, SpatialInertia, SpatialTransform, Vec3};
294 use phyz_model::ModelBuilder;
295
296 fn make_pendulum() -> Model {
297 ModelBuilder::new()
298 .gravity(Vec3::new(0.0, -GRAVITY, 0.0))
299 .dt(0.001)
300 .add_revolute_body(
301 "link1",
302 -1,
303 SpatialTransform::identity(),
304 SpatialInertia::new(
305 1.0,
306 Vec3::new(0.0, -0.5, 0.0),
307 Mat3::from_diagonal(&Vec3::new(1.0 / 12.0, 0.0, 1.0 / 12.0)),
308 ),
309 )
310 .build()
311 }
312
313 #[test]
314 fn test_jacobians_match() {
315 let model = make_pendulum();
316 let mut state = model.default_state();
317 state.q[0] = 0.3;
318 state.v[0] = 0.1;
319
320 let fd = finite_diff_jacobians(&model, &state, 1e-6);
321 let an = semi_implicit_step_jacobians(&model, &state);
322
323 let eps = 1e-4;
325 assert!(
326 (&fd.dqnext_dq - &an.dqnext_dq).norm() < eps,
327 "dqnext_dq mismatch: fd={:?}, an={:?}",
328 fd.dqnext_dq,
329 an.dqnext_dq
330 );
331 assert!(
332 (&fd.dvnext_dq - &an.dvnext_dq).norm() < eps,
333 "dvnext_dq mismatch"
334 );
335 assert!(
336 (&fd.dvnext_dv - &an.dvnext_dv).norm() < eps,
337 "dvnext_dv mismatch"
338 );
339 assert!(
340 (&fd.dvnext_dctrl - &an.dvnext_dctrl).norm() < eps,
341 "dvnext_dctrl mismatch"
342 );
343 }
344}