1use phyz_math::{DMat, DVec};
7use phyz_model::{Model, State};
8use phyz_rigid::aba;
9
10#[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
27pub 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 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 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 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 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
98pub 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 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 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
184fn 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 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}