rby1-sdk
Loading...
Searching...
No Matches
optimal_control.h
1#pragma once
2
3#include <Eigen/Dense>
4#include <iostream>
5#include <optional>
6
7#include "rby1-sdk/dynamics/robot.h"
8#include "rby1-sdk/export.h"
9#include "rby1-sdk/math/qp_solver.h"
10#include "rby1-sdk/math/se3.h"
11
12namespace rb {
13
14template <int DOF>
16 public:
17 enum class ExitCode : int { kNoError = 0, kInequalityConstraintViolation, kQPSolverError };
18
19 struct LinkTarget {
20 int ref_link_index{};
21 int link_index{};
22 math::SE3::MatrixType T{math::SE3::Identity()};
23 double weight_position{0.};
24 double weight_orientation{0.};
25 };
26
27 struct COMTarget {
28 int ref_link_index{};
29 Eigen::Vector3d com{Eigen::Vector3d::Zero()};
30 double weight{0.};
31 };
32
34 explicit JointAngleTarget(int dof) {
35 q = Eigen::Vector<double, DOF>::Zero(dof);
36 weight = Eigen::Vector<double, DOF>::Zero(dof);
37 }
38
40 if constexpr (DOF > 0) {
41 q.setZero();
42 weight.setZero();
43 } else {
44 static_assert(DOF > 0, "JointAngleTarget() is only available for fixed-size DOF");
45 }
46 }
47
48 Eigen::Vector<double, DOF> q;
49 Eigen::Vector<double, DOF> weight;
50 };
51
53 explicit NullspaceJointTarget(int dof) {
54 q = Eigen::Vector<double, DOF>::Zero(dof);
55 weight = Eigen::Vector<double, DOF>::Zero(dof);
56 }
57
59 if constexpr (DOF > 0) {
60 q.setZero();
61 weight.setZero();
62 } else {
63 static_assert(DOF > 0, "NullspaceJointTarget() is only available for fixed-size DOF");
64 }
65 }
66
67 Eigen::Vector<double, DOF> q;
68 Eigen::Vector<double, DOF> weight;
69 double k_p{0.2};
70 double k_d{0.2};
71 double cost_weight{1e-3};
72 };
73
74 struct Input {
75 std::optional<std::vector<LinkTarget>> link_targets;
76 std::optional<COMTarget> com_target;
77 std::optional<JointAngleTarget> q_target;
78 std::optional<NullspaceJointTarget> nullspace_q_target;
79 };
80
81 explicit OptimalControl(std::shared_ptr<dyn::Robot<DOF>> robot, //
82 const std::vector<unsigned int>& joint_idx, //
83 bool nullspace_mapping = true, //
84 bool soft_position_boundary = false //
85 )
86 : robot_(std::move(robot)),
87 joint_idx_(joint_idx),
88 dof_(robot_->GetDOF()),
89 n_joints_((int)joint_idx.size()),
90 nullspace_mapping_(nullspace_mapping),
91 soft_position_boundary_(soft_position_boundary) {
92 n_vars_ = soft_position_boundary_ ? 3 * n_joints_ : n_joints_;
93 n_consts_ = soft_position_boundary_ ? 5 * n_joints_ : n_joints_;
94 qp_solver_.Setup(n_vars_, n_consts_);
95
96 std::vector<bool> is_selected(dof_, false);
97 for (const auto& i : joint_idx_) {
98 if (i < dof_) {
99 is_selected[i] = true;
100 }
101 }
102 unselected_joint_idx_.reserve(dof_ - n_joints_);
103 for (int i = 0; i < dof_; i++) {
104 if (!is_selected[i]) {
105 unselected_joint_idx_.push_back(i);
106 }
107 }
108 }
109
110 std::optional<Eigen::VectorXd> Solve(Input in, //
111 std::shared_ptr<dyn::State<DOF>> state, //
112 double dt, //
113 double error_scaling, //
114 const Eigen::Vector<double, DOF>& position_lower_limit, //
115 const Eigen::Vector<double, DOF>& position_upper_limit, //
116 const Eigen::Vector<double, DOF>& velocity_limit, //
117 const Eigen::Vector<double, DOF>& acceleration_limit, //
118 bool need_forward_kinematics = false //
119 ) {
120 exit_code_ = ExitCode::kNoError;
121 exit_code_msg_ = "";
122
123 double err_sum = 0;
124 double max_cond = 1.0;
125
126 dt = std::max(dt, 1.e-6);
127
128 if (need_forward_kinematics) {
129 robot_->ComputeForwardKinematics(state);
130 }
131
132 qp_solver_.InitFunction();
133
134 if (in.link_targets.has_value()) {
135 AddLinkTargetCosts(in.link_targets.value(), state, dt, error_scaling, err_sum, max_cond);
136 }
137
138 if (in.com_target.has_value()) {
139 AddCOMTargetCost(in.com_target.value(), state, dt, error_scaling, err_sum, max_cond);
140 }
141
142 if (in.q_target.has_value()) {
143 AddJointTargetCost(in.q_target.value(), state, dt, error_scaling, err_sum);
144 }
145
146 // Snapshot of the cost before adding nullspace cost
147 const Eigen::MatrixXd A_cost_primary = qp_solver_.GetACost();
148 const Eigen::MatrixXd J_primary = A_cost_primary.leftCols(n_joints_);
149
150 if (in.nullspace_q_target) {
151 AddNullspaceCostTowardQTarget_Projected(*in.nullspace_q_target, state, dt, J_primary);
152 }
153
154 if (nullspace_mapping_) {
155 AddNullspaceCost_Projected(1e-4, J_primary);
156 }
157
158 if (soft_position_boundary_) {
159 const double weight = 1.;
160 const double safety_margin = 5 * math::kDeg2Rad;
161
162 Eigen::VectorXd q = state->GetQ();
163
164 Eigen::MatrixXd upper_penalty = Eigen::MatrixXd::Zero(n_joints_, n_vars_);
165 Eigen::MatrixXd lower_penalty = Eigen::MatrixXd::Zero(n_joints_, n_vars_);
166 for (int i = 0; i < n_joints_; i++) {
167 if (q(joint_idx_[i]) > position_upper_limit(joint_idx_[i]) - safety_margin) {
168 const double alpha = std::clamp(
169 (safety_margin - (position_upper_limit(joint_idx_[i]) - q(joint_idx_[i]))) / safety_margin, 1.e-6, 1.0);
170 upper_penalty(i, n_joints_ + i) = weight * alpha;
171 } else {
172 upper_penalty(i, n_joints_ + i) = 1.e-6; // DUMMY FOR PATTERN LOCK
173 }
174 if (q(joint_idx_[i]) < position_lower_limit(joint_idx_[i]) + safety_margin) {
175 const double alpha = std::clamp(
176 (safety_margin - (q(joint_idx_[i]) - position_lower_limit(joint_idx_[i]))) / safety_margin, 1.e-6, 1.0);
177 lower_penalty(i, 2 * n_joints_ + i) = weight * alpha;
178 } else {
179 lower_penalty(i, 2 * n_joints_ + i) = 1.e-6; // DUMMY FOR PATTERN LOCK
180 }
181 }
182 qp_solver_.AddCostFunction(upper_penalty, Eigen::VectorXd::Zero(n_joints_));
183 qp_solver_.AddCostFunction(lower_penalty, Eigen::VectorXd::Zero(n_joints_));
184 }
185
186 qp_solver_.SetCostFunction(qp_solver_.GetACost(), qp_solver_.GetBCost());
187
188 if (!SetInequalityConstraints(state->GetQ()(joint_idx_), //
189 state->GetQdot()(joint_idx_), //
190 position_lower_limit(joint_idx_), //
191 position_upper_limit(joint_idx_), //
192 velocity_limit(joint_idx_), //
193 acceleration_limit(joint_idx_), //
194 dt //
195 )) {
196 return {};
197 }
198
199 Eigen::VectorXd primal_variable = Eigen::VectorXd::Zero(n_vars_);
200 primal_variable.head(n_joints_) = state->GetQdot()(joint_idx_);
201 qp_solver_.SetPrimalVariable(primal_variable);
202
203 try {
204 Eigen::VectorXd solution = state->GetQdot();
205 const auto& result = qp_solver_.Solve();
206 solution(joint_idx_) = result.head(n_joints_);
207 err_ = std::sqrt(err_sum);
208 manipulability_ = max_cond;
209 return solution;
210 } catch (math::QPSolverException& e) {
211 std::stringstream ss;
212 ss << "[OptimalControl::Solve] OSQP Error: " << e.what();
213 exit_code_ = ExitCode::kQPSolverError;
214 exit_code_msg_ = ss.str();
215 return {};
216 }
217 }
218
219 std::optional<Eigen::VectorXd> Solve(Input in, //
220 std::shared_ptr<dyn::State<DOF>> state, //
221 double dt, //
222 double error_scaling, //
223 const Eigen::Vector<double, DOF>& velocity_limit, //
224 const Eigen::Vector<double, DOF>& acceleration_limit, //
225 bool need_forward_kinematics = false //
226 ) {
227 return Solve(in, state, dt, error_scaling, robot_->GetLimitQLower(state), robot_->GetLimitQUpper(state),
228 velocity_limit, acceleration_limit, need_forward_kinematics);
229 }
230
231 ExitCode GetExitCode() const { return exit_code_; }
232
233 std::string GetExitCodeMessage() const { return exit_code_msg_; }
234
235 double GetError() const { return err_; }
236
237 double GetManipulability() const { return manipulability_; }
238
239 protected:
240 template <int N>
241 double CalculateConditionNumber(const Eigen::Matrix<double, N, N>& A) {
242 constexpr double kEpsilon = 1e-6;
243
244 Eigen::JacobiSVD<Eigen::Matrix<double, N, N>> svd(A);
245 const auto& singular_values = svd.singularValues();
246
247 double max_sv = singular_values.maxCoeff();
248 double min_sv = singular_values.minCoeff();
249
250 if (std::abs(min_sv) <= kEpsilon) {
251 return std::numeric_limits<double>::infinity();
252 }
253 return max_sv / min_sv;
254 }
255
256 Eigen::MatrixXd ComputeNullspaceProjection(const Eigen::MatrixXd& J) {
257 Eigen::MatrixXd JJt = J * J.transpose();
258 Eigen::MatrixXd pinv = J.transpose() * JJt.completeOrthogonalDecomposition().pseudoInverse();
259
260 Eigen::MatrixXd I = Eigen::MatrixXd::Identity(J.cols(), J.cols());
261 return I - pinv * J;
262 }
263
264 void AddLinkTargetCosts(const std::vector<LinkTarget>& link_targets, //
265 const std::shared_ptr<dyn::State<DOF>>& state, //
266 double dt, //
267 double error_scaling, //
268 double& err_sum, //
269 double& max_cond //
270 ) {
271 using namespace math;
272
273 Eigen::Vector<double, DOF> qdot = state->GetQdot();
274 qdot(unselected_joint_idx_).setZero();
275
276 for (const auto& link_target : link_targets) {
277 Eigen::Matrix4d T_cur = robot_->ComputeTransformation(state, link_target.ref_link_index, link_target.link_index);
278 Eigen::Matrix4d T_err = T_cur.inverse() * link_target.T;
279
280 Eigen::Matrix<double, 6, DOF> J_full =
281 robot_->ComputeBodyJacobian(state, link_target.ref_link_index, link_target.link_index);
282 J_full(Eigen::all, unselected_joint_idx_).setZero();
283
284 Eigen::Vector<double, 6> w;
285 w.head<3>().fill(link_target.weight_orientation < 1e-6 ? 0. : link_target.weight_orientation);
286 w.tail<3>().fill(link_target.weight_position < 1e-6 ? 0. : link_target.weight_position);
287
288 Eigen::Matrix<double, 6, 6> JJt = J_full(Eigen::all, joint_idx_) * J_full(Eigen::all, joint_idx_).transpose();
289 max_cond = std::max(max_cond, CalculateConditionNumber(JJt));
290
291 Eigen::Vector<double, 6> err = w.asDiagonal() * (SE3::Log(T_err) / dt * error_scaling - J_full * qdot / 2);
292 Eigen::Matrix<double, 6, DOF> J_weighted = w.asDiagonal() * J_full / 2;
293 err_sum += err.squaredNorm();
294
295 Eigen::MatrixXd A{Eigen::MatrixXd::Zero(6, n_vars_)};
296 A.block(0, 0, 6, n_joints_) = J_weighted(Eigen::all, joint_idx_);
297
298 qp_solver_.AddCostFunction(A, err);
299 }
300 }
301
302 void AddCOMTargetCost(const COMTarget& target, //
303 const std::shared_ptr<dyn::State<DOF>>& state, //
304 double dt, //
305 double error_scaling, //
306 double& err_sum, //
307 double& max_cond //
308 ) {
309 Eigen::Vector<double, DOF> qdot = state->GetQdot();
310 qdot(unselected_joint_idx_).setZero();
311
312 Eigen::Vector3d com = robot_->ComputeCenterOfMass(state, target.ref_link_index);
313 Eigen::Matrix<double, 3, DOF> J_com = robot_->ComputeCenterOfMassJacobian(state, target.ref_link_index);
314 J_com(Eigen::all, unselected_joint_idx_).setZero();
315
316 Eigen::Vector<double, 3> w;
317 w.fill(target.weight < 1e-6 ? 0. : target.weight);
318
319 Eigen::Matrix3d JJt = J_com * J_com.transpose();
320 max_cond = std::max(max_cond, CalculateConditionNumber(JJt));
321
322 Eigen::Vector3d err = w.asDiagonal() * ((target.com - com) / dt * error_scaling - J_com * qdot / 2);
323 Eigen::Matrix<double, 3, DOF> J_com_weighted = w.asDiagonal() * J_com / 2;
324 err_sum += err.squaredNorm();
325
326 Eigen::MatrixXd A{Eigen::MatrixXd::Zero(3, n_vars_)};
327 A.block(0, 0, 3, n_joints_) = J_com_weighted(Eigen::all, joint_idx_);
328
329 qp_solver_.AddCostFunction(A, err);
330 }
331
332 void AddJointTargetCost(const JointAngleTarget& target, //
333 const std::shared_ptr<dyn::State<DOF>>& state, //
334 double dt, //
335 double error_scaling, //
336 double& err_sum //
337 ) {
338 Eigen::VectorXd q = state->GetQ()(joint_idx_);
339 Eigen::VectorXd qdot = state->GetQdot()(joint_idx_);
340
341 Eigen::MatrixXd J_full = Eigen::MatrixXd::Identity(n_joints_, n_joints_);
342
343 Eigen::MatrixXd w = Eigen::MatrixXd::Zero(n_joints_, n_joints_);
344 for (int i = 0; i < n_joints_; i++) {
345 if (target.weight[joint_idx_[i]] > 1e-6) {
346 w(i, i) = target.weight[joint_idx_[i]];
347 }
348 }
349
350 Eigen::VectorXd err = w * ((target.q(joint_idx_) - q) / dt * error_scaling - qdot / 2);
351 Eigen::MatrixXd J_weighted = w * J_full / 2;
352 err_sum += err.squaredNorm();
353
354 Eigen::MatrixXd A{Eigen::MatrixXd::Zero(n_joints_, n_vars_)};
355 A.block(0, 0, n_joints_, n_joints_) = J_weighted;
356
357 qp_solver_.AddCostFunction(A, err);
358 }
359
360 void AddNullspaceCost_Projected(double weight, const Eigen::MatrixXd& J_primary) {
361 Eigen::MatrixXd P = ComputeNullspaceProjection(J_primary);
362 Eigen::MatrixXd A = Eigen::MatrixXd::Zero(n_joints_, n_vars_);
363 A.block(0, 0, n_joints_, n_joints_) = weight * P;
364 qp_solver_.AddCostFunction(A, Eigen::VectorXd::Zero(n_joints_));
365 }
366
367 void AddNullspaceCostToward_Projected(const Eigen::Vector<double, DOF>& v_des_full, double weight,
368 const Eigen::MatrixXd& J_primary) {
369 const Eigen::MatrixXd P = ComputeNullspaceProjection(J_primary);
370 Eigen::VectorXd v_des = v_des_full(joint_idx_);
371 Eigen::MatrixXd A = Eigen::MatrixXd::Zero(n_joints_, n_vars_);
372 A.block(0, 0, n_joints_, n_joints_) = weight * P;
373 Eigen::VectorXd b = weight * P * v_des;
374 qp_solver_.AddCostFunction(A, b);
375 }
376
377 void AddNullspaceCostTowardQTarget_Projected(const NullspaceJointTarget& target,
378 const std::shared_ptr<dyn::State<DOF>>& state, double dt,
379 const Eigen::MatrixXd& J_primary) {
380 const double h = std::max(dt, 1e-6);
381 const Eigen::VectorXd q = state->GetQ();
382 const Eigen::VectorXd qdot = state->GetQdot();
383
384 Eigen::Vector<double, DOF> v_des;
385 if constexpr (DOF > 0) {
386 v_des.setZero();
387 } else {
388 v_des = Eigen::Vector<double, DOF>::Zero(q.size());
389 }
390 v_des(joint_idx_) = target.k_p * (target.q(joint_idx_) - q(joint_idx_)) / h - target.k_d * qdot(joint_idx_);
391
392 const Eigen::MatrixXd P = ComputeNullspaceProjection(J_primary);
393
394 if ((target.weight.array() > 1e-6).any()) {
395 Eigen::VectorXd w = target.weight(joint_idx_).cwiseMax(0.0);
396 Eigen::MatrixXd W = w.asDiagonal();
397 Eigen::MatrixXd A = Eigen::MatrixXd::Zero(n_joints_, n_vars_);
398 A.block(0, 0, n_joints_, n_joints_) = target.cost_weight * (W * P);
399 Eigen::VectorXd b = target.cost_weight * (W * P) * v_des(joint_idx_);
400 qp_solver_.AddCostFunction(A, b);
401 } else {
402 AddNullspaceCostToward_Projected(v_des, target.cost_weight, J_primary);
403 }
404 }
405
406 bool SetInequalityConstraints(const Eigen::VectorXd& q, //
407 const Eigen::VectorXd& qdot, //
408 const Eigen::VectorXd& q_lb, //
409 const Eigen::VectorXd& q_ub, //
410 const Eigen::VectorXd& velocity_limit, //
411 const Eigen::VectorXd& acceleration_limit, //
412 double dt //
413 ) {
414 Eigen::MatrixXd A_const = Eigen::MatrixXd::Zero(n_consts_, n_vars_);
415 A_const.block(0, 0, n_joints_, n_joints_).setIdentity();
416 if (soft_position_boundary_) {
417 A_const.block(n_joints_, 0, n_joints_, n_joints_) = Eigen::VectorXd::Constant(n_joints_, -1).asDiagonal();
418 A_const.block(n_joints_, n_joints_, n_joints_, n_joints_).setIdentity();
419 A_const.block(2 * n_joints_, n_joints_, n_joints_, n_joints_).setIdentity();
420 A_const.block(3 * n_joints_, 0, n_joints_, n_joints_).setIdentity();
421 A_const.block(3 * n_joints_, 2 * n_joints_, n_joints_, n_joints_).setIdentity();
422 A_const.block(4 * n_joints_, 2 * n_joints_, n_joints_, n_joints_).setIdentity();
423 }
424
425 Eigen::VectorXd qdot_lb = -velocity_limit;
426 Eigen::VectorXd qdot_ub = velocity_limit;
427 qdot_lb = qdot_lb.cwiseMax(qdot - acceleration_limit * dt);
428 qdot_ub = qdot_ub.cwiseMin(qdot + acceleration_limit * dt);
429 qdot_lb = qdot_lb.cwiseMax(2 * (q_lb - q) / dt - qdot);
430 qdot_ub = qdot_ub.cwiseMin(2 * (q_ub - q) / dt - qdot);
431
432 if ((qdot_lb.array() <= qdot_ub.array()).all()) {
433 Eigen::VectorXd lb = Eigen::VectorXd::Zero(n_consts_);
434 Eigen::VectorXd ub = Eigen::VectorXd::Zero(n_consts_);
435
436 lb.head(n_joints_) = qdot_lb;
437 ub.head(n_joints_) = qdot_ub;
438 lb.tail(n_consts_ - n_joints_).setZero();
439 ub.tail(n_consts_ - n_joints_).setConstant(1e6);
440
441 qp_solver_.SetConstraintsFunction(A_const, lb, ub);
442
443 return true;
444 }
445
446 Eigen::IOFormat eigen_fmt(Eigen::StreamPrecision, Eigen::DontAlignCols, ", ", ", ", "", "", "", "");
447 std::stringstream ss;
448 ss << "Inequality constraint violation - qdot_lb: [" << qdot_lb.format(eigen_fmt) << "], qdot_ub: ["
449 << qdot_ub.format(eigen_fmt) << "]";
450 exit_code_ = ExitCode::kInequalityConstraintViolation;
451 exit_code_msg_ = ss.str();
452 return false;
453 }
454
455 private:
456 ExitCode exit_code_{ExitCode::kNoError};
457 std::string exit_code_msg_{};
458
459 double err_{};
460 double manipulability_{};
461
462 math::QPSolver qp_solver_;
463
464 std::shared_ptr<dyn::Robot<DOF>> robot_;
465 int dof_{};
466 int n_joints_{};
467 int n_vars_{};
468 int n_consts_{};
469 bool nullspace_mapping_{false};
470 bool soft_position_boundary_{false};
471
472 std::vector<unsigned int> joint_idx_;
473 std::vector<unsigned int> unselected_joint_idx_;
474};
475
476} // namespace rb
Definition optimal_control.h:15
Definition state.h:10
Eigen::VectorXd Solve()
Definition optimal_control.h:27
Definition optimal_control.h:74
Definition optimal_control.h:33
Definition optimal_control.h:52