Skip to content

Commit 7deffff

Browse files
Fix LogDDP single-shooting regressions (#166)
Align the Python bindings with the new LogBarrierOptions surface and re-roll nominal trajectories for both cold and warm starts. Add regression coverage for inconsistent cold-start guesses and reused warm-start trajectories.
1 parent 4a9621f commit 7deffff

5 files changed

Lines changed: 170 additions & 121 deletions

File tree

include/cddp-cpp/cddp_core/logddp_solver.hpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ class LogDDPSolver : public CDDPSolverBase {
5656
void printIteration(int iter, const CDDP &context) const override;
5757

5858
private:
59-
// Dynamics storage (forward-simulated trajectory)
60-
std::vector<Eigen::VectorXd> F_;
61-
6259
// Constraint values g(x,u) - g_ub
6360
std::map<std::string, std::vector<Eigen::VectorXd>> G_;
6461

@@ -70,9 +67,6 @@ class LogDDPSolver : public CDDPSolverBase {
7067
// Filter-based line search
7168
double constraint_violation_;
7269

73-
// Multi-shooting parameters
74-
int ms_segment_length_;
75-
7670
/**
7771
* @brief Evaluate trajectory by computing cost, dynamics, and merit function.
7872
*/

include/cddp-cpp/cddp_core/options.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,10 @@ namespace cddp
130130
};
131131

132132
/**
133-
* @brief Options for the relaxed log-barrier method.
133+
* @brief Options for the relaxed log-barrier method (single-shooting).
134134
*/
135-
struct LogBarrierOptions : MultiShootingOptions
135+
struct LogBarrierOptions
136136
{
137-
LogBarrierOptions() { use_controlled_rollout = true; }
138-
139137
bool use_relaxed_log_barrier_penalty =
140138
false; ///< Use relaxed log-barrier method.
141139
double relaxed_log_barrier_delta =

python/src/bind_options.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,9 @@ void bind_options(py::module_& m) {
7474
.def_readwrite("slack_var_init_scale", &cddp::InteriorPointOptions::slack_var_init_scale)
7575
.def_readwrite("barrier", &cddp::InteriorPointOptions::barrier);
7676

77-
// LogBarrierOptions exposed as a flat Python binding; no Python base class.
77+
// LogBarrierOptions exposed as a flat Python binding; single-shooting only.
7878
py::class_<cddp::LogBarrierOptions>(m, "LogBarrierOptions")
7979
.def(py::init<>())
80-
.def_readwrite("segment_length", &cddp::LogBarrierOptions::segment_length)
81-
.def_readwrite("rollout_type", &cddp::LogBarrierOptions::rollout_type)
82-
.def_readwrite("use_controlled_rollout", &cddp::LogBarrierOptions::use_controlled_rollout)
83-
.def_readwrite("costate_var_init_scale", &cddp::LogBarrierOptions::costate_var_init_scale)
8480
.def_readwrite("use_relaxed_log_barrier_penalty", &cddp::LogBarrierOptions::use_relaxed_log_barrier_penalty)
8581
.def_readwrite("relaxed_log_barrier_delta", &cddp::LogBarrierOptions::relaxed_log_barrier_delta)
8682
.def_readwrite("barrier", &cddp::LogBarrierOptions::barrier);

src/cddp_core/logddp_solver.cpp

Lines changed: 41 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,22 @@ limitations under the License.
2525

2626
namespace cddp {
2727

28+
namespace {
29+
30+
void rollOutNominalTrajectory(CDDP &context) {
31+
const int horizon = context.getHorizon();
32+
33+
context.X_[0] = context.getInitialState();
34+
for (int t = 0; t < horizon; ++t) {
35+
context.X_[t + 1] = context.getSystem().getDiscreteDynamics(
36+
context.X_[t], context.U_[t], t * context.getTimestep());
37+
}
38+
}
39+
40+
} // namespace
41+
2842
LogDDPSolver::LogDDPSolver()
29-
: mu_(1e-1), relaxation_delta_(1e-5), constraint_violation_(1e+7),
30-
ms_segment_length_(5) {}
43+
: mu_(1e-1), relaxation_delta_(1e-5), constraint_violation_(1e+7) {}
3144

3245
void LogDDPSolver::initialize(CDDP &context) {
3346
const CDDPOptions &options = context.getOptions();
@@ -52,14 +65,13 @@ void LogDDPSolver::initialize(CDDP &context) {
5265
bool valid_warm_start =
5366
(k_u_.size() == static_cast<size_t>(horizon) &&
5467
K_u_.size() == static_cast<size_t>(horizon) &&
55-
F_.size() == static_cast<size_t>(horizon) &&
5668
context.X_.size() == static_cast<size_t>(horizon + 1) &&
5769
context.U_.size() == static_cast<size_t>(horizon));
5870

5971
if (valid_warm_start && !k_u_.empty()) {
6072
for (int t = 0; t < horizon; ++t) {
6173
if (k_u_[t].size() != control_dim || K_u_[t].rows() != control_dim ||
62-
K_u_[t].cols() != state_dim || F_[t].size() != state_dim) {
74+
K_u_[t].cols() != state_dim) {
6375
valid_warm_start = false;
6476
break;
6577
}
@@ -99,7 +111,11 @@ void LogDDPSolver::initialize(CDDP &context) {
99111
dV_ = Eigen::Vector2d::Zero();
100112

101113
constraint_violation_ = std::numeric_limits<double>::infinity();
102-
ms_segment_length_ = options.log_barrier.segment_length;
114+
115+
// Warm starts may reuse gains with a user-modified state trajectory.
116+
// Re-roll the nominal state sequence so the linearization point stays
117+
// dynamically consistent after defect tracking removal.
118+
rollOutNominalTrajectory(context);
103119

104120
// Evaluate current trajectory and reset filter
105121
evaluateTrajectory(context);
@@ -112,8 +128,9 @@ void LogDDPSolver::initialize(CDDP &context) {
112128
}
113129
}
114130

115-
// Cold start: Initialize trajectories with interpolation between initial and
116-
// reference states
131+
// Cold start: initialize a nominal trajectory and always roll it out from
132+
// the current control guess so the linearization point is dynamically
133+
// consistent, even when the user provided a same-sized state guess.
117134
if (context.X_.size() != static_cast<size_t>(horizon + 1) ||
118135
context.U_.size() != static_cast<size_t>(horizon)) {
119136
context.X_.resize(horizon + 1);
@@ -133,11 +150,9 @@ void LogDDPSolver::initialize(CDDP &context) {
133150
}
134151
}
135152

136-
// Resize dynamics storage
137-
F_.resize(horizon);
138-
for (int t = 0; t < horizon; ++t) {
139-
F_[t] = Eigen::VectorXd::Zero(state_dim);
140-
}
153+
// Roll out the nominal trajectory for all cold starts, including user-
154+
// supplied trajectories whose state sequence is only an initial guess.
155+
rollOutNominalTrajectory(context);
141156

142157
// Use base class helper for gains
143158
initializeGains(horizon, control_dim, state_dim);
@@ -173,28 +188,15 @@ void LogDDPSolver::initialize(CDDP &context) {
173188

174189
constraint_violation_ = std::numeric_limits<double>::infinity();
175190

176-
ms_segment_length_ = options.log_barrier.segment_length;
177-
178-
// Check if ms_segment_length_ is valid
179-
if (ms_segment_length_ < 0) {
180-
std::cerr << "LogDDP: ms_segment_length_ must be non-negative" << std::endl;
181-
throw std::runtime_error("LogDDP: ms_segment_length_ must be non-negative");
182-
}
183-
184-
const std::string &rollout_type = options.log_barrier.rollout_type;
185-
if (rollout_type != "linear" && rollout_type != "nonlinear" &&
186-
rollout_type != "hybrid") {
187-
std::cerr << "LogDDP: Invalid ms_rollout_type: " << rollout_type
188-
<< std::endl;
189-
throw std::runtime_error("LogDDP: Invalid ms_rollout_type");
190-
}
191-
192191
// Initialize log barrier object
193192
mu_ = options.log_barrier.barrier.mu_initial;
194193
relaxation_delta_ = options.log_barrier.relaxed_log_barrier_delta;
195194
if (!relaxed_log_barrier_) {
196195
relaxed_log_barrier_ =
197196
std::make_unique<RelaxedLogBarrier>(mu_, relaxation_delta_);
197+
} else {
198+
relaxed_log_barrier_->setBarrierCoeff(mu_);
199+
relaxed_log_barrier_->setRelaxationDelta(relaxation_delta_);
198200
}
199201

200202
// Evaluate initial trajectory
@@ -225,9 +227,6 @@ void LogDDPSolver::applyForwardPassResult(CDDP &context,
225227
CDDPSolverBase::applyForwardPassResult(context, result);
226228

227229
// Also update LogDDP-specific state
228-
if (result.dynamics_trajectory) {
229-
F_ = *result.dynamics_trajectory;
230-
}
231230
constraint_violation_ = result.constraint_violation;
232231
}
233232

@@ -319,17 +318,9 @@ void LogDDPSolver::evaluateTrajectory(CDDP &context) {
319318
const int horizon = context.getHorizon();
320319
double cost = 0.0;
321320

322-
// Rollout dynamics and calculate cost
321+
// Calculate cost over the (already consistent) trajectory
323322
for (int t = 0; t < horizon; ++t) {
324-
const Eigen::VectorXd &x_t = context.X_[t];
325-
const Eigen::VectorXd &u_t = context.U_[t];
326-
327-
// Compute stage cost using the guessed state/control
328-
cost += context.getObjective().running_cost(x_t, u_t, t);
329-
330-
// Compute dynamics
331-
F_[t] = context.getSystem().getDiscreteDynamics(x_t, u_t,
332-
t * context.getTimestep());
323+
cost += context.getObjective().running_cost(context.X_[t], context.U_[t], t);
333324
}
334325

335326
// Add terminal cost based on the final guessed state
@@ -343,7 +334,6 @@ void LogDDPSolver::resetFilter(CDDP &context) {
343334
// Evaluate log-barrier cost (includes path constraints)
344335
context.merit_function_ = context.cost_;
345336
constraint_violation_ = 0.0;
346-
double defect_violation = 0.0;
347337

348338
const auto &constraint_set = context.getConstraintSet();
349339

@@ -364,12 +354,7 @@ void LogDDPSolver::resetFilter(CDDP &context) {
364354
}
365355
}
366356
}
367-
368-
// Add defect violation penalty
369-
Eigen::VectorXd d = F_[t] - context.X_[t + 1];
370-
defect_violation += d.lpNorm<1>();
371357
}
372-
constraint_violation_ += defect_violation;
373358
context.inf_pr_ = constraint_violation_;
374359
}
375360

@@ -502,8 +487,6 @@ bool LogDDPSolver::backwardPass(CDDP &context) {
502487
for (int t = horizon - 1; t >= 0; --t) {
503488
const Eigen::VectorXd &x = context.X_[t];
504489
const Eigen::VectorXd &u = context.U_[t];
505-
const Eigen::VectorXd &f = F_[t];
506-
const Eigen::VectorXd &d = f - context.X_[t + 1]; // Defect
507490

508491
// Use pre-computed continuous-time dynamics Jacobians
509492
const Eigen::MatrixXd &Fx = F_x_[t];
@@ -517,8 +500,8 @@ bool LogDDPSolver::backwardPass(CDDP &context) {
517500
auto [l_xx, l_uu, l_ux] =
518501
context.getObjective().getRunningCostHessians(x, u, t);
519502

520-
Eigen::VectorXd Q_x = l_x + A.transpose() * (V_x + V_xx * d);
521-
Eigen::VectorXd Q_u = l_u + B.transpose() * (V_x + V_xx * d);
503+
Eigen::VectorXd Q_x = l_x + A.transpose() * V_x;
504+
Eigen::VectorXd Q_u = l_u + B.transpose() * V_x;
522505
Eigen::MatrixXd Q_xx = l_xx + A.transpose() * V_xx * A;
523506
Eigen::MatrixXd Q_ux = l_ux + B.transpose() * V_xx * A;
524507
Eigen::MatrixXd Q_uu = l_uu + B.transpose() * V_xx * B;
@@ -631,61 +614,22 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
631614
result.control_trajectory = context.U_;
632615
result.state_trajectory[0] = context.getInitialState();
633616

634-
std::vector<Eigen::VectorXd> F_new = F_;
635-
636617
double cost_new = 0.0;
637618
double merit_function_new = 0.0;
638619
double rp_err = 0.0;
639-
double rf_err = 0.0;
640620

641-
// Rollout loop with multi-shooting logic from original
621+
// Single-shooting rollout
642622
for (int t = 0; t < horizon; ++t) {
643623
const Eigen::VectorXd delta_x_t =
644624
result.state_trajectory[t] - context.X_[t];
645625

646-
// Update control
647626
result.control_trajectory[t] =
648627
context.U_[t] + alpha * k_u_[t] + K_u_[t] * delta_x_t;
649628

650-
// --- Rollout Logic from original ---
651-
Eigen::VectorXd dynamics_eval_for_F_new_t;
652-
653-
// Determine if the *next* step (t+1) starts a new segment boundary
654-
bool is_segment_boundary = (ms_segment_length_ > 0) &&
655-
((t + 1) % ms_segment_length_ == 0) &&
656-
(t + 1 < horizon);
657-
bool apply_gap_closing_strategy = is_segment_boundary;
658-
659-
if (apply_gap_closing_strategy) {
660-
if (options.log_barrier.rollout_type == "nonlinear") {
661-
F_new[t] = context.getSystem().getDiscreteDynamics(
662-
result.state_trajectory[t], result.control_trajectory[t],
663-
t * context.getTimestep());
664-
result.state_trajectory[t + 1] = context.X_[t + 1] +
665-
(F_new[t] - F_[t]) +
666-
alpha * (F_[t] - context.X_[t + 1]);
667-
} else if (options.log_barrier.rollout_type == "hybrid") {
668-
F_new[t] = context.getSystem().getDiscreteDynamics(
669-
result.state_trajectory[t], result.control_trajectory[t],
670-
t * context.getTimestep());
671-
const auto [Fx, Fu] = context.getSystem().getJacobians(
672-
context.X_[t], context.U_[t], t * context.getTimestep());
673-
const Eigen::MatrixXd A =
674-
Eigen::MatrixXd::Identity(state_dim, state_dim) +
675-
context.getTimestep() * Fx;
676-
const Eigen::MatrixXd B = context.getTimestep() * Fu;
677-
result.state_trajectory[t + 1] =
678-
context.X_[t + 1] + (A + B * K_u_[t]) * delta_x_t +
679-
alpha * (B * k_u_[t] + F_[t] - context.X_[t + 1]);
680-
}
681-
} else {
682-
result.state_trajectory[t + 1] = context.getSystem().getDiscreteDynamics(
683-
result.state_trajectory[t], result.control_trajectory[t],
684-
t * context.getTimestep());
685-
F_new[t] = result.state_trajectory[t + 1];
686-
}
629+
result.state_trajectory[t + 1] = context.getSystem().getDiscreteDynamics(
630+
result.state_trajectory[t], result.control_trajectory[t],
631+
t * context.getTimestep());
687632

688-
// Robustness check
689633
if (!result.state_trajectory[t + 1].allFinite() ||
690634
!result.control_trajectory[t].allFinite()) {
691635
if (options.debug) {
@@ -698,13 +642,12 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
698642
}
699643
}
700644

701-
// Cost computation and filter line-search from original
645+
// Cost computation and constraint evaluation
702646
for (int t = 0; t < horizon; ++t) {
703647
cost_new += context.getObjective().running_cost(
704648
result.state_trajectory[t], result.control_trajectory[t], t);
705649

706650
for (const auto &constraint_pair : constraint_set) {
707-
const std::string &constraint_name = constraint_pair.first;
708651
Eigen::VectorXd g_t =
709652
constraint_pair.second->evaluate(result.state_trajectory[t],
710653
result.control_trajectory[t]) -
@@ -719,18 +662,15 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
719662
}
720663
}
721664
}
722-
723-
Eigen::VectorXd d = F_new[t] - result.state_trajectory[t + 1];
724-
rf_err += d.lpNorm<1>();
725665
}
726666

727667
cost_new +=
728668
context.getObjective().terminal_cost(result.state_trajectory.back());
729669
merit_function_new += cost_new;
730670

731-
// Filter-based acceptance using original logic with new options structure
671+
// Filter-based acceptance
732672
double constraint_violation_old = constraint_violation_;
733-
double constraint_violation_new = rf_err + rp_err;
673+
double constraint_violation_new = rp_err;
734674
double merit_function_old = context.merit_function_;
735675
bool filter_acceptance = false;
736676
double expected_improvement = alpha * dV_(0);
@@ -766,7 +706,6 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
766706
result.cost = cost_new;
767707
result.merit_function = merit_function_new;
768708
result.constraint_violation = constraint_violation_new;
769-
result.dynamics_trajectory = F_new;
770709
}
771710

772711
return result;

0 commit comments

Comments
 (0)