Skip to content

Commit 43fdba2

Browse files
Use ID_FO_AZA-style backward sweep for inner-term computation
The mass-matrix derivative inner products required by the second-order forward dynamics formula are now computed via an O(N^2) per-column articulated-body recursion instead of the dense (dM/dq) * (dqdd/d{q,v,tau}) matrix-matrix products. Following Singh, Russell & Wensing, "On Second-Order Derivatives of Rigid-Body Dynamics: Theory & Implementation" (T-RO 2023), Fig. 14-15, the recursive Inner-Term becomes preferable to the dense DTM/DMM product beyond N ~ 25 for tree topologies, while the Outer-Term keeps the dense -M^{-1} * term_in GEMM (BLAS-3) since that is faster at practical robotics sizes. The recursion mirrors the ID_FO_AZA algorithm in the reference, with the base-joint spatial acceleration deliberately set to zero so the result contains only the mass-matrix derivative term (no gravity contribution). Algorithm public API and Data layout are unchanged. Refs: #2691
1 parent 9ee347d commit 43fdba2

1 file changed

Lines changed: 235 additions & 94 deletions

File tree

include/pinocchio/src/algorithm/aba-second-order-derivatives.hxx

Lines changed: 235 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,160 @@
1111
#include "pinocchio/algorithm/aba-second-order-derivatives.hpp"
1212
#endif // PINOCCHIO_LSP
1313

14+
#include "pinocchio/spatial/act-on-set.hpp"
15+
1416
namespace pinocchio
1517
{
1618

19+
// Forward sweep used by ComputeABASecondOrderDerivatives to set up the
20+
// articulated-body-style inner-product recursion: forward kinematics,
21+
// CRBA composite inertia, and the spatial force corresponding to the
22+
// input acceleration `a`. Gravity is deliberately zeroed at the base so
23+
// the result of the subsequent backward sweep is (dM/dq)*a only -- no
24+
// gravity contribution.
25+
template<
26+
typename Scalar,
27+
int Options,
28+
template<typename, int> class JointCollectionTpl,
29+
typename ConfigVectorType,
30+
typename InputAccType>
31+
struct ComputeABASecondOrderDerivativesInnerForwardStep
32+
: public fusion::JointUnaryVisitorBase<ComputeABASecondOrderDerivativesInnerForwardStep<
33+
Scalar,
34+
Options,
35+
JointCollectionTpl,
36+
ConfigVectorType,
37+
InputAccType>>
38+
{
39+
typedef ModelTpl<Scalar, Options, JointCollectionTpl> Model;
40+
typedef DataTpl<Scalar, Options, JointCollectionTpl> Data;
41+
42+
typedef boost::fusion::vector<
43+
const Model &,
44+
Data &,
45+
const ConfigVectorType &,
46+
const InputAccType &>
47+
ArgsType;
48+
49+
template<typename JointModel>
50+
static void algo(
51+
const JointModelBase<JointModel> & jmodel,
52+
JointDataBase<typename JointModel::JointDataDerived> & jdata,
53+
const Model & model,
54+
Data & data,
55+
const Eigen::MatrixBase<ConfigVectorType> & q,
56+
const Eigen::MatrixBase<InputAccType> & a)
57+
{
58+
typedef typename Model::JointIndex JointIndex;
59+
typedef typename Data::Motion Motion;
60+
typedef typename Data::Inertia Inertia;
61+
62+
const JointIndex i = jmodel.id();
63+
const JointIndex parent = model.parents[i];
64+
Motion & oa = data.oa[i];
65+
66+
jmodel.calc(jdata.derived(), q.derived());
67+
data.liMi[i] = model.jointPlacements[i] * jdata.M();
68+
69+
if (parent > 0)
70+
{
71+
data.oMi[i] = data.oMi[parent] * data.liMi[i];
72+
oa = data.oa[parent];
73+
}
74+
else
75+
{
76+
data.oMi[i] = data.liMi[i];
77+
oa.setZero(); // zero gravity: result is (dM/dq)*a, not (dM/dq)*a + dh/dq
78+
}
79+
80+
typedef
81+
typename SizeDepType<JointModel::NV>::template ColsReturn<typename Data::Matrix6x>::Type
82+
ColsBlock;
83+
ColsBlock J_cols = jmodel.jointCols(data.J);
84+
ColsBlock ddJ_cols = jmodel.jointCols(data.ddJ);
85+
86+
J_cols.noalias() = data.oMi[i].act(jdata.S());
87+
motionSet::motionAction(oa, J_cols, ddJ_cols);
88+
oa += data.oMi[i].act(jdata.S() * jmodel.jointVelocitySelector(a));
89+
90+
Inertia & oY = data.oYcrb[i];
91+
oY = data.oMi[i].act(model.inertias[i]);
92+
data.of[i] = oY * oa;
93+
}
94+
};
95+
96+
// Backward sweep companion to ComputeABASecondOrderDerivativesInnerForwardStep.
97+
// Fills `dM_a_dq(j, k) = d/dq_k ( M(q) * a )_j` using `Ftmp1` and `Ftmp3`
98+
// as accumulation workspace (each sized 6 x model.nv).
99+
template<
100+
typename Scalar,
101+
int Options,
102+
template<typename, int> class JointCollectionTpl,
103+
typename FtmpType,
104+
typename OutputMatrixType>
105+
struct ComputeABASecondOrderDerivativesInnerBackwardStep
106+
: public fusion::JointUnaryVisitorBase<ComputeABASecondOrderDerivativesInnerBackwardStep<
107+
Scalar,
108+
Options,
109+
JointCollectionTpl,
110+
FtmpType,
111+
OutputMatrixType>>
112+
{
113+
typedef ModelTpl<Scalar, Options, JointCollectionTpl> Model;
114+
typedef DataTpl<Scalar, Options, JointCollectionTpl> Data;
115+
116+
typedef boost::fusion::
117+
vector<const Model &, Data &, FtmpType &, FtmpType &, OutputMatrixType &>
118+
ArgsType;
119+
120+
template<typename JointModel>
121+
static void algo(
122+
const JointModelBase<JointModel> & jmodel,
123+
const Model & model,
124+
Data & data,
125+
FtmpType & Ftmp1,
126+
FtmpType & Ftmp3,
127+
OutputMatrixType & dM_a_dq)
128+
{
129+
typedef typename Model::JointIndex JointIndex;
130+
131+
const JointIndex i = jmodel.id();
132+
const JointIndex parent = model.parents[i];
133+
134+
typedef
135+
typename SizeDepType<JointModel::NV>::template ColsReturn<typename Data::Matrix6x>::Type
136+
ColsBlock;
137+
ColsBlock J_cols = jmodel.jointCols(data.J);
138+
ColsBlock ddJ_cols = jmodel.jointCols(data.ddJ);
139+
ColsBlock tmp1 = jmodel.jointCols(Ftmp1);
140+
ColsBlock tmp3 = jmodel.jointCols(Ftmp3);
141+
142+
const Eigen::Index joint_idx = (Eigen::Index)jmodel.idx_v();
143+
const Eigen::Index joint_dofs = (Eigen::Index)jmodel.nv();
144+
const Eigen::Index subtree_dofs = (Eigen::Index)data.nvSubtree[i];
145+
const Eigen::Index successor_idx = joint_idx + joint_dofs;
146+
const Eigen::Index successor_dofs = subtree_dofs - joint_dofs;
147+
148+
typename Data::Inertia & oYcrb = data.oYcrb[i];
149+
motionSet::inertiaAction(oYcrb, J_cols, tmp1);
150+
motionSet::inertiaAction<ADDTO>(oYcrb, ddJ_cols, tmp3);
151+
motionSet::act<ADDTO>(J_cols, data.of[i], tmp3);
152+
153+
if (successor_dofs > 0)
154+
dM_a_dq.block(joint_idx, successor_idx, joint_dofs, successor_dofs).noalias() =
155+
J_cols.transpose() * Ftmp3.middleCols(successor_idx, successor_dofs);
156+
157+
dM_a_dq.block(joint_idx, joint_idx, subtree_dofs, joint_dofs).noalias() =
158+
Ftmp1.middleCols(joint_idx, subtree_dofs).transpose() * ddJ_cols;
159+
160+
if (parent > 0)
161+
{
162+
data.oYcrb[parent] += data.oYcrb[i];
163+
data.of[parent] += data.of[i];
164+
}
165+
}
166+
};
167+
17168
template<
18169
typename Scalar,
19170
int Options,
@@ -36,132 +187,122 @@ namespace pinocchio
36187
const Tensor3 & d2ddq_dqdv,
37188
const Tensor4 & d2ddq_dtaudq)
38189
{
39-
assert(q.size() == model.nq && "The joint configuration vector is not of right size");
40-
assert(v.size() == model.nv && "The joint velocity vector is not of right size");
41-
assert(tau.size() == model.nv && "The joint torque vector is not of right size");
42-
assert(d2ddq_dqdq.dimension(0) == model.nv);
43-
assert(d2ddq_dqdq.dimension(1) == model.nv);
44-
assert(d2ddq_dqdq.dimension(2) == model.nv);
45-
assert(d2ddq_dvdv.dimension(0) == model.nv);
46-
assert(d2ddq_dvdv.dimension(1) == model.nv);
47-
assert(d2ddq_dvdv.dimension(2) == model.nv);
48-
assert(d2ddq_dqdv.dimension(0) == model.nv);
49-
assert(d2ddq_dqdv.dimension(1) == model.nv);
50-
assert(d2ddq_dqdv.dimension(2) == model.nv);
51-
assert(d2ddq_dtaudq.dimension(0) == model.nv);
52-
assert(d2ddq_dtaudq.dimension(1) == model.nv);
53-
assert(d2ddq_dtaudq.dimension(2) == model.nv);
190+
PINOCCHIO_CHECK_ARGUMENT_SIZE(q.size(), model.nq);
191+
PINOCCHIO_CHECK_ARGUMENT_SIZE(v.size(), model.nv);
192+
PINOCCHIO_CHECK_ARGUMENT_SIZE(tau.size(), model.nv);
193+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dqdq.dimension(0), model.nv);
194+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dqdq.dimension(1), model.nv);
195+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dqdq.dimension(2), model.nv);
196+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dvdv.dimension(0), model.nv);
197+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dvdv.dimension(1), model.nv);
198+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dvdv.dimension(2), model.nv);
199+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dqdv.dimension(0), model.nv);
200+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dqdv.dimension(1), model.nv);
201+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dqdv.dimension(2), model.nv);
202+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dtaudq.dimension(0), model.nv);
203+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dtaudq.dimension(1), model.nv);
204+
PINOCCHIO_CHECK_ARGUMENT_SIZE(d2ddq_dtaudq.dimension(2), model.nv);
54205
assert(model.check(data) && "data is not consistent with model.");
206+
assert(model.check(MimicChecker()) && "Function does not support mimic joints");
55207

56-
typedef typename DataTpl<Scalar, Options, JointCollectionTpl>::MatrixXs MatrixXs;
57-
typedef typename DataTpl<Scalar, Options, JointCollectionTpl>::VectorXs VectorXs;
58-
typedef typename DataTpl<Scalar, Options, JointCollectionTpl>::Tensor3x Tensor3x;
59-
typedef Eigen::Map<MatrixXs> MatrixMap;
60-
typedef Eigen::Map<const MatrixXs> ConstMatrixMap;
61-
typedef Eigen::Map<VectorXs> VectorMap;
62-
typedef Eigen::Map<const VectorXs> ConstVectorMap;
208+
typedef ModelTpl<Scalar, Options, JointCollectionTpl> Model;
209+
typedef DataTpl<Scalar, Options, JointCollectionTpl> Data;
210+
typedef typename Data::MatrixXs MatrixXs;
211+
typedef typename Data::VectorXs VectorXs;
212+
typedef typename Data::Matrix6x Matrix6x;
213+
typedef typename Data::Tensor3x Tensor3x;
214+
typedef typename Model::JointIndex JointIndex;
63215

64216
const Eigen::Index nv = model.nv;
65217
const Eigen::Index nv2 = nv * nv;
66218

67-
// 1. First-order ABA derivatives: data.ddq_dq, data.ddq_dv and data.Minv
68-
// (upper triangular). Also leaves data.ddq filled.
219+
// 1) First-order ABA derivatives: fills data.ddq, data.ddq_dq, data.ddq_dv
220+
// and (upper triangle of) data.Minv.
69221
computeABADerivatives(model, data, q.derived(), v.derived(), tau.derived());
70-
71-
// computeABADerivatives only fills the upper triangle of Minv (which is
72-
// symmetric). Symmetrize so it can be used directly as the outer multiplier.
73222
data.Minv.template triangularView<Eigen::StrictlyLower>() =
74223
data.Minv.transpose().template triangularView<Eigen::StrictlyLower>();
75224

76-
// 2. Second-order RNEA derivatives, evaluated at (q, v, qdd). The 4th
77-
// output d2tau_dadq carries the pages of dM/dq.
225+
// 2) Second-order RNEA derivatives at (q, v, ddq). d2tau_dadq holds the
226+
// pages of dM/dq.
78227
ComputeRNEASecondOrderDerivatives(
79228
model, data, q.derived(), v.derived(), data.ddq, data.d2tau_dqdq, data.d2tau_dvdv,
80229
data.d2tau_dqdv, data.d2tau_dadq);
81230

82-
// 3. Pre-compute the three inner products used by the chain-rule formulas.
83-
// Index conventions are kept consistent with the rest of the SO API:
84-
// prodq(n, a, b) = (M_q_a * ddq_dq.col(b))_n
85-
// prodv(n, a, b) = (M_q_a * ddq_dv.col(b))_n
86-
// prodqdd(n, c, u) = (M_q_u * Minv)(n, c)
87-
// where M_q_u(n, m) = d2tau_dadq(n, m, u) is the u-th page of dM/dq.
88-
Tensor3x prodq(nv, nv, nv);
89-
Tensor3x prodv(nv, nv, nv);
90-
Tensor3x prodqdd(nv, nv, nv);
231+
// 3) Inner-Term: for each column w of (ddq_dq, ddq_dv, Minv), compute
232+
// (dM/dq) * column and store as page w of the corresponding tensor.
233+
typedef ComputeABASecondOrderDerivativesInnerForwardStep<
234+
Scalar, Options, JointCollectionTpl, ConfigVectorType, VectorXs>
235+
InnerPass1;
236+
typedef ComputeABASecondOrderDerivativesInnerBackwardStep<
237+
Scalar, Options, JointCollectionTpl, Matrix6x, MatrixXs>
238+
InnerPass2;
91239

92-
for (Eigen::Index u = 0; u < nv; ++u)
93-
{
94-
ConstMatrixMap Mq_u(data.d2tau_dadq.data() + u * nv2, nv, nv);
240+
Tensor3x prodq(nv, nv, nv), prodv(nv, nv, nv), prodqdd(nv, nv, nv);
241+
Matrix6x Ftmp1 = Matrix6x::Zero(6, nv);
242+
Matrix6x Ftmp3 = Matrix6x::Zero(6, nv);
243+
MatrixXs inner_out(nv, nv);
244+
245+
auto fill_inner = [&](const auto & input_columns, Tensor3x & out_tensor) {
95246
for (Eigen::Index w = 0; w < nv; ++w)
96247
{
97-
VectorMap prodq_uw(prodq.data() + (w * nv + u) * nv, nv); // prodq(:, u, w)
98-
VectorMap prodv_uw(prodv.data() + (w * nv + u) * nv, nv); // prodv(:, u, w)
99-
prodq_uw.noalias() = Mq_u * data.ddq_dq.col(w);
100-
prodv_uw.noalias() = Mq_u * data.ddq_dv.col(w);
248+
VectorXs a_input = input_columns.col(w);
249+
for (JointIndex i = 1; i < (JointIndex)model.njoints; ++i)
250+
InnerPass1::run(
251+
model.joints[i], data.joints[i],
252+
typename InnerPass1::ArgsType(model, data, q.derived(), a_input));
253+
254+
Ftmp1.setZero();
255+
Ftmp3.setZero();
256+
inner_out.setZero();
257+
for (JointIndex i = (JointIndex)(model.njoints - 1); i > 0; --i)
258+
InnerPass2::run(
259+
model.joints[i],
260+
typename InnerPass2::ArgsType(model, data, Ftmp1, Ftmp3, inner_out));
261+
262+
Eigen::Map<MatrixXs>(out_tensor.data() + w * nv2, nv, nv) = inner_out;
101263
}
102-
MatrixMap prodqdd_u(prodqdd.data() + u * nv2, nv, nv); // prodqdd page u
103-
prodqdd_u.noalias() = Mq_u * data.Minv;
104-
}
264+
};
105265

106-
// 4. Assemble the inner block term_in of size (nv, 4 * nv * nv). For each
107-
// outer index u and inner index i, four nv-vectors are written:
108-
// (4u ) i : d2tau_dqdq(:, i, u) + prodq(:, u, i) + prodq(:, i, u)
109-
// (4u + 1) i : d2tau_dvdv(:, i, u)
110-
// (4u + 2) i : d2tau_dqdv(:, i, u) + prodv(:, i, u)
111-
// (4u + 3) i : prodqdd(:, i, u)
112-
//
113-
// The 2nd-dim slice of a Tensor3x is not contiguous in memory, so we
114-
// use column-by-column maps for the prodq cross term.
266+
fill_inner(data.ddq_dq, prodq);
267+
fill_inner(data.ddq_dv, prodv);
268+
fill_inner(data.Minv, prodqdd);
269+
270+
// 4) Outer-Term: build term_in (nv x 4*nv*nv) and take a single dense
271+
// -M^{-1} * term_in product. Singh, Russell & Wensing (2023),
272+
// Fig. 14b: at practical robotics sizes the BLAS-3 GEMM beats the
273+
// AZA recursion on the Outer-Term.
115274
MatrixXs term_in(nv, 4 * nv2);
116275
for (Eigen::Index u = 0; u < nv; ++u)
117276
{
118-
const Scalar * d2tau_dqdq_page = data.d2tau_dqdq.data() + u * nv2;
119-
const Scalar * d2tau_dvdv_page = data.d2tau_dvdv.data() + u * nv2;
120-
const Scalar * d2tau_dqdv_page = data.d2tau_dqdv.data() + u * nv2;
121-
const Scalar * prodv_page = prodv.data() + u * nv2;
122-
const Scalar * prodqdd_page = prodqdd.data() + u * nv2;
123-
124277
for (Eigen::Index i = 0; i < nv; ++i)
125278
{
126-
// prodq(:, u, i) is the contiguous column at offset (i * nv + u) * nv
127-
ConstVectorMap prodq_u_i(prodq.data() + (i * nv + u) * nv, nv);
128-
// prodq(:, i, u) is the contiguous column at offset (u * nv + i) * nv
129-
ConstVectorMap prodq_i_u(prodq.data() + (u * nv + i) * nv, nv);
130-
131-
ConstVectorMap d2tau_dqdq_iu(d2tau_dqdq_page + i * nv, nv);
132-
ConstVectorMap d2tau_dvdv_iu(d2tau_dvdv_page + i * nv, nv);
133-
ConstVectorMap d2tau_dqdv_iu(d2tau_dqdv_page + i * nv, nv);
134-
ConstVectorMap prodv_iu(prodv_page + i * nv, nv);
135-
ConstVectorMap prodqdd_iu(prodqdd_page + i * nv, nv);
136-
137-
term_in.col((4 * u) * nv + i) = d2tau_dqdq_iu + prodq_u_i + prodq_i_u;
138-
term_in.col((4 * u + 1) * nv + i) = d2tau_dvdv_iu;
139-
term_in.col((4 * u + 2) * nv + i) = d2tau_dqdv_iu + prodv_iu;
140-
term_in.col((4 * u + 3) * nv + i) = prodqdd_iu;
279+
for (Eigen::Index n = 0; n < nv; ++n)
280+
{
281+
term_in(n, (4 * u + 0) * nv + i) =
282+
data.d2tau_dqdq(n, i, u) + prodq(n, u, i) + prodq(n, i, u);
283+
term_in(n, (4 * u + 1) * nv + i) = data.d2tau_dvdv(n, i, u);
284+
term_in(n, (4 * u + 2) * nv + i) = data.d2tau_dqdv(n, i, u) + prodv(n, i, u);
285+
term_in(n, (4 * u + 3) * nv + i) = prodqdd(n, u, i);
286+
}
141287
}
142288
}
143289

144-
// 5. Outer DMM: term_out = -Minv * term_in. Single dense matrix product.
145290
MatrixXs term_out(nv, 4 * nv2);
146291
term_out.noalias() = -data.Minv * term_in;
147292

148-
// 6. Scatter term_out columns back into the four output tensors.
149-
Tensor1 & d2ddq_dqdq_ = const_cast<Tensor1 &>(d2ddq_dqdq);
150-
Tensor2 & d2ddq_dvdv_ = const_cast<Tensor2 &>(d2ddq_dvdv);
151-
Tensor3 & d2ddq_dqdv_ = const_cast<Tensor3 &>(d2ddq_dqdv);
152-
Tensor4 & d2ddq_dtaudq_ = const_cast<Tensor4 &>(d2ddq_dtaudq);
153-
293+
// 5) Scatter term_out into the four output tensors.
154294
for (Eigen::Index u = 0; u < nv; ++u)
155295
{
156-
MatrixMap dqdq_u(d2ddq_dqdq_.data() + u * nv2, nv, nv);
157-
MatrixMap dvdv_u(d2ddq_dvdv_.data() + u * nv2, nv, nv);
158-
MatrixMap dqdv_u(d2ddq_dqdv_.data() + u * nv2, nv, nv);
159-
MatrixMap dtdq_u(d2ddq_dtaudq_.data() + u * nv2, nv, nv);
160-
161-
dqdq_u = term_out.middleCols((4 * u) * nv, nv);
162-
dvdv_u = term_out.middleCols((4 * u + 1) * nv, nv);
163-
dqdv_u = term_out.middleCols((4 * u + 2) * nv, nv);
164-
dtdq_u = term_out.middleCols((4 * u + 3) * nv, nv);
296+
for (Eigen::Index i = 0; i < nv; ++i)
297+
{
298+
for (Eigen::Index n = 0; n < nv; ++n)
299+
{
300+
const_cast<Tensor1 &>(d2ddq_dqdq)(n, i, u) = term_out(n, (4 * u + 0) * nv + i);
301+
const_cast<Tensor2 &>(d2ddq_dvdv)(n, i, u) = term_out(n, (4 * u + 1) * nv + i);
302+
const_cast<Tensor3 &>(d2ddq_dqdv)(n, i, u) = term_out(n, (4 * u + 2) * nv + i);
303+
const_cast<Tensor4 &>(d2ddq_dtaudq)(n, i, u) = term_out(n, (4 * u + 3) * nv + i);
304+
}
305+
}
165306
}
166307
}
167308

0 commit comments

Comments
 (0)