Skip to main content

phyz_diff/
lib.rs

1//! Differentiation utilities for tau: Jacobians of dynamics.
2//!
3//! Provides both finite-difference and (future) analytical derivatives
4//! of the equations of motion.
5
6use phyz_math::{DMat, DVec};
7use phyz_model::{Model, State};
8use phyz_rigid::aba;
9
10/// Jacobians of a single simulation step.
11///
12/// For state (q, v) → (q', v') after one step:
13/// - `dqnext_dq`: ∂q'/∂q
14/// - `dqnext_dv`: ∂q'/∂v
15/// - `dvnext_dq`: ∂v'/∂q
16/// - `dvnext_dv`: ∂v'/∂v
17/// - `dvnext_dctrl`: ∂v'/∂ctrl
18#[derive(Debug, Clone)]
19pub struct StepJacobians {
20    pub dqnext_dq: DMat,
21    pub dqnext_dv: DMat,
22    pub dvnext_dq: DMat,
23    pub dvnext_dv: DMat,
24    pub dvnext_dctrl: DMat,
25}
26
27/// Compute step Jacobians via finite differences.
28///
29/// Perturbs each component of (q, v, ctrl) and measures the change in (q', v')
30/// after a semi-implicit Euler step.
31pub fn finite_diff_jacobians(model: &Model, state: &State, eps: f64) -> StepJacobians {
32    let nq = model.nq;
33    let nv = model.nv;
34    let dt = model.dt;
35
36    // Nominal step (used as reference for central differences)
37    let (_q_nom, _v_nom) = semi_implicit_euler_step(model, state, dt);
38
39    let mut dqnext_dq = DMat::zeros(nq, nq);
40    let mut dqnext_dv = DMat::zeros(nq, nv);
41    let mut dvnext_dq = DMat::zeros(nv, nq);
42    let mut dvnext_dv = DMat::zeros(nv, nv);
43    let mut dvnext_dctrl = DMat::zeros(nv, nv);
44
45    // Perturb q
46    for j in 0..nq {
47        let mut s_plus = state.clone();
48        s_plus.q[j] += eps;
49        let (qp, vp) = semi_implicit_euler_step(model, &s_plus, dt);
50
51        let mut s_minus = state.clone();
52        s_minus.q[j] -= eps;
53        let (qm, vm) = semi_implicit_euler_step(model, &s_minus, dt);
54
55        let inv_2eps = 1.0 / (2.0 * eps);
56        dqnext_dq.set_column(j, &((&qp - &qm) * inv_2eps));
57        dvnext_dq.set_column(j, &((&vp - &vm) * inv_2eps));
58    }
59
60    // Perturb v
61    for j in 0..nv {
62        let mut s_plus = state.clone();
63        s_plus.v[j] += eps;
64        let (qp, vp) = semi_implicit_euler_step(model, &s_plus, dt);
65
66        let mut s_minus = state.clone();
67        s_minus.v[j] -= eps;
68        let (qm, vm) = semi_implicit_euler_step(model, &s_minus, dt);
69
70        let inv_2eps = 1.0 / (2.0 * eps);
71        dqnext_dv.set_column(j, &((&qp - &qm) * inv_2eps));
72        dvnext_dv.set_column(j, &((&vp - &vm) * inv_2eps));
73    }
74
75    // Perturb ctrl
76    for j in 0..nv {
77        let mut s_plus = state.clone();
78        s_plus.ctrl[j] += eps;
79        let (_, vp) = semi_implicit_euler_step(model, &s_plus, dt);
80
81        let mut s_minus = state.clone();
82        s_minus.ctrl[j] -= eps;
83        let (_, vm) = semi_implicit_euler_step(model, &s_minus, dt);
84
85        let inv_2eps = 1.0 / (2.0 * eps);
86        dvnext_dctrl.set_column(j, &((&vp - &vm) * inv_2eps));
87    }
88
89    StepJacobians {
90        dqnext_dq,
91        dqnext_dv,
92        dvnext_dq,
93        dvnext_dv,
94        dvnext_dctrl,
95    }
96}
97
98/// Compute analytical step Jacobians for semi-implicit Euler.
99///
100/// Semi-implicit Euler:
101///   qdd = ABA(q, v, ctrl)
102///   v' = v + dt * qdd
103///   q' = q + dt * v'
104///
105/// Derivatives via chain rule through ABA.
106/// Uses finite differences on ABA itself for now (analytical ABA derivatives in future PR).
107pub fn analytical_step_jacobians(model: &Model, state: &State) -> StepJacobians {
108    let nq = model.nq;
109    let nv = model.nv;
110    let dt = model.dt;
111    let eps = 1e-7;
112
113    // Compute ABA Jacobians via finite differences on the acceleration function
114    let _qdd_nom = aba(model, state);
115
116    let mut dqdd_dq = DMat::zeros(nv, nq);
117    let mut dqdd_dv = DMat::zeros(nv, nv);
118    let mut dqdd_dctrl = DMat::zeros(nv, nv);
119
120    for j in 0..nq {
121        let mut sp = state.clone();
122        sp.q[j] += eps;
123        let qddp = aba(model, &sp);
124
125        let mut sm = state.clone();
126        sm.q[j] -= eps;
127        let qddm = aba(model, &sm);
128
129        dqdd_dq.set_column(j, &((&qddp - &qddm) / (2.0 * eps)));
130    }
131
132    for j in 0..nv {
133        let mut sp = state.clone();
134        sp.v[j] += eps;
135        let qddp = aba(model, &sp);
136
137        let mut sm = state.clone();
138        sm.v[j] -= eps;
139        let qddm = aba(model, &sm);
140
141        dqdd_dv.set_column(j, &((&qddp - &qddm) / (2.0 * eps)));
142    }
143
144    for j in 0..nv {
145        let mut sp = state.clone();
146        sp.ctrl[j] += eps;
147        let qddp = aba(model, &sp);
148
149        let mut sm = state.clone();
150        sm.ctrl[j] -= eps;
151        let qddm = aba(model, &sm);
152
153        dqdd_dctrl.set_column(j, &((&qddp - &qddm) / (2.0 * eps)));
154    }
155
156    // Semi-implicit Euler derivatives:
157    // v' = v + dt * qdd(q, v, ctrl)
158    // q' = q + dt * v'
159    //
160    // dv'/dq = dt * dqdd/dq
161    // dv'/dv = I + dt * dqdd/dv
162    // dv'/dctrl = dt * dqdd/dctrl
163    //
164    // dq'/dq = I + dt * dv'/dq = I + dt² * dqdd/dq
165    // dq'/dv = dt * (I + dt * dqdd/dv) = dt * dv'/dv
166    let i_nv = DMat::identity(nv, nv);
167    let i_nq = DMat::identity(nq, nq);
168
169    let dvnext_dq = &dqdd_dq * dt;
170    let dvnext_dv = &i_nv + &dqdd_dv * dt;
171    let dvnext_dctrl = &dqdd_dctrl * dt;
172    let dqnext_dq = &i_nq + &dvnext_dq * dt;
173    let dqnext_dv = &dvnext_dv * dt;
174
175    StepJacobians {
176        dqnext_dq,
177        dqnext_dv,
178        dvnext_dq,
179        dvnext_dv,
180        dvnext_dctrl,
181    }
182}
183
184/// Semi-implicit Euler step (returns new q, v).
185fn semi_implicit_euler_step(model: &Model, state: &State, dt: f64) -> (DVec, DVec) {
186    let qdd = aba(model, state);
187    let v_new = &state.v + &qdd * dt;
188    let q_new = &state.q + &v_new * dt;
189    (q_new, v_new)
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use phyz_math::{GRAVITY, Mat3, SpatialInertia, SpatialTransform, Vec3};
196    use phyz_model::ModelBuilder;
197
198    fn make_pendulum() -> Model {
199        ModelBuilder::new()
200            .gravity(Vec3::new(0.0, -GRAVITY, 0.0))
201            .dt(0.001)
202            .add_revolute_body(
203                "link1",
204                -1,
205                SpatialTransform::identity(),
206                SpatialInertia::new(
207                    1.0,
208                    Vec3::new(0.0, -0.5, 0.0),
209                    Mat3::from_diagonal(&Vec3::new(1.0 / 12.0, 0.0, 1.0 / 12.0)),
210                ),
211            )
212            .build()
213    }
214
215    #[test]
216    fn test_jacobians_match() {
217        let model = make_pendulum();
218        let mut state = model.default_state();
219        state.q[0] = 0.3;
220        state.v[0] = 0.1;
221
222        let fd = finite_diff_jacobians(&model, &state, 1e-6);
223        let an = analytical_step_jacobians(&model, &state);
224
225        // Check that analytical matches finite-diff
226        let eps = 1e-4;
227        assert!(
228            (&fd.dqnext_dq - &an.dqnext_dq).norm() < eps,
229            "dqnext_dq mismatch: fd={:.6}, an={:.6}",
230            fd.dqnext_dq,
231            an.dqnext_dq
232        );
233        assert!(
234            (&fd.dvnext_dq - &an.dvnext_dq).norm() < eps,
235            "dvnext_dq mismatch"
236        );
237        assert!(
238            (&fd.dvnext_dv - &an.dvnext_dv).norm() < eps,
239            "dvnext_dv mismatch"
240        );
241        assert!(
242            (&fd.dvnext_dctrl - &an.dvnext_dctrl).norm() < eps,
243            "dvnext_dctrl mismatch"
244        );
245    }
246}