Skip to main content

phyz_diff/
lib.rs

1//! Differentiation utilities for phyz: Jacobians of the equations of motion.
2//!
3//! Three routines compute the same object — the Jacobians of one semi-implicit
4//! Euler step — by different means, trading accuracy against cost:
5//!
6//! | Function | Method | Exact? |
7//! | --- | --- | --- |
8//! | [`finite_diff_jacobians`] | central differences over the whole step | no |
9//! | [`semi_implicit_step_jacobians`] | chain rule through the integrator, central differences on ABA | no (ABA block is FD) |
10//! | [`symbolic::symbolic_step_jacobians`] | symbolic differentiation of the dynamics | yes |
11//!
12//! Only the symbolic path is derivative-exact. The other two are finite
13//! differences and inherit the usual step-size/round-off tradeoff. For exact
14//! *parameter* and contact-surface gradients over a whole trajectory, see the
15//! dual-number adjoint in [`phyz::diff`](https://docs.rs/phyz) instead.
16//!
17//! # Example
18//!
19//! ```
20//! use phyz_diff::semi_implicit_step_jacobians;
21//! use phyz_math::{GRAVITY, Mat3, SpatialInertia, SpatialTransform, Vec3};
22//! use phyz_model::ModelBuilder;
23//!
24//! let model = ModelBuilder::new()
25//!     .gravity(Vec3::new(0.0, -GRAVITY, 0.0))
26//!     .dt(0.002)
27//!     .add_revolute_body(
28//!         "pendulum",
29//!         -1,
30//!         SpatialTransform::identity(),
31//!         SpatialInertia::new(
32//!             1.0,
33//!             Vec3::new(0.0, -0.5, 0.0),
34//!             Mat3::from_diagonal(&Vec3::new(0.083, 0.0, 0.083)),
35//!         ),
36//!     )
37//!     .build();
38//!
39//! let mut state = model.default_state();
40//! state.q[0] = 0.3;
41//!
42//! let jac = semi_implicit_step_jacobians(&model, &state);
43//! assert_eq!(jac.dvnext_dq.nrows(), model.nv);
44//! ```
45
46#![warn(missing_docs)]
47
48// Compile the crate README's Rust blocks as doc-tests so the documented API
49// cannot drift from the real one. `cfg(doctest)` keeps it out of rendered docs.
50#[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
59// The rollout adjoint API is the crate's headline surface; hoist it to the
60// root so callers can write `phyz_diff::AdjointRollout` (and so the `phyz`
61// facade's `phyz::diff::…` spelling keeps working).
62pub use rollout::{
63    AdjointGradients, AdjointRollout, CollisionMesh, ContactSetup, FinalStateObjective,
64    GroundContact, N_INERTIA_PARAMS, adjoint_rollout_gradient, inertia_params, rollout_objective,
65};
66
67// The unified adjoint through the convex contact solve — the same contact
68// model `phyz::Simulator::step_with_contacts` integrates forward.
69pub 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/// Jacobians of a single simulation step.
79///
80/// For state (q, v) -> (q', v') after one step:
81/// - `dqnext_dq`: dq'/dq
82/// - `dqnext_dv`: dq'/dv
83/// - `dvnext_dq`: dv'/dq
84/// - `dvnext_dv`: dv'/dv
85/// - `dvnext_dctrl`: dv'/dctrl
86#[derive(Debug, Clone)]
87pub struct StepJacobians {
88    /// dq'/dq — sensitivity of next positions to current positions.
89    pub dqnext_dq: DMat,
90    /// dq'/dv — sensitivity of next positions to current velocities.
91    pub dqnext_dv: DMat,
92    /// dv'/dq — sensitivity of next velocities to current positions.
93    pub dvnext_dq: DMat,
94    /// dv'/dv — sensitivity of next velocities to current velocities.
95    pub dvnext_dv: DMat,
96    /// dv'/dctrl — sensitivity of next velocities to applied joint torques.
97    pub dvnext_dctrl: DMat,
98}
99
100/// Set column `j` of matrix `m` from DVec `col`.
101fn 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
107/// Compute step Jacobians via finite differences.
108///
109/// Perturbs each component of (q, v, ctrl) and measures the change in (q', v')
110/// after a semi-implicit Euler step.
111pub 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    // Nominal step (used as reference for central differences)
117    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    // Perturb q
128    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    // Perturb v
142    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    // Perturb ctrl
156    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 alias for [`semi_implicit_step_jacobians`].
178///
179/// The old name claimed the result was analytical; the ABA block is computed
180/// by finite differences.
181#[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
189/// Compute step Jacobians for semi-implicit Euler by chain rule through the
190/// integrator.
191///
192/// Semi-implicit Euler:
193///   qdd = ABA(q, v, ctrl)
194///   v' = v + dt * qdd
195///   q' = q + dt * v'
196///
197/// The integrator layer is differentiated exactly; the ABA block
198/// (∂qdd/∂q, ∂qdd/∂v, ∂qdd/∂ctrl) is still obtained by **central finite
199/// differences** on `aba`, so this routine is *not* derivative-exact. It is
200/// cheaper and better-conditioned than [`finite_diff_jacobians`], which
201/// perturbs the whole step, but it carries the same truncation/round-off
202/// tradeoff. For exact derivatives use
203/// [`symbolic::symbolic_step_jacobians`].
204pub 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    // Compute ABA Jacobians via finite differences on the acceleration function
212    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    // Semi-implicit Euler derivatives:
255    // v' = v + dt * qdd(q, v, ctrl)
256    // q' = q + dt * v'
257    //
258    // dv'/dq = dt * dqdd/dq
259    // dv'/dv = I + dt * dqdd/dv
260    // dv'/dctrl = dt * dqdd/dctrl
261    //
262    // dq'/dq = I + dt * dv'/dq = I + dt^2 * dqdd/dq
263    // dq'/dv = dt * (I + dt * dqdd/dv) = dt * dv'/dv
264    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
282/// Semi-implicit Euler step (returns new q, v).
283fn 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        // Check that analytical matches finite-diff
324        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}