Skip to content

Commit 4106bb4

Browse files
Fix LogDDP single-shooting regressions
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 4106bb4

5 files changed

Lines changed: 167 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: 38 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,22 +188,6 @@ 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;
@@ -225,9 +224,6 @@ void LogDDPSolver::applyForwardPassResult(CDDP &context,
225224
CDDPSolverBase::applyForwardPassResult(context, result);
226225

227226
// Also update LogDDP-specific state
228-
if (result.dynamics_trajectory) {
229-
F_ = *result.dynamics_trajectory;
230-
}
231227
constraint_violation_ = result.constraint_violation;
232228
}
233229

@@ -319,17 +315,9 @@ void LogDDPSolver::evaluateTrajectory(CDDP &context) {
319315
const int horizon = context.getHorizon();
320316
double cost = 0.0;
321317

322-
// Rollout dynamics and calculate cost
318+
// Calculate cost over the (already consistent) trajectory
323319
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());
320+
cost += context.getObjective().running_cost(context.X_[t], context.U_[t], t);
333321
}
334322

335323
// Add terminal cost based on the final guessed state
@@ -343,7 +331,6 @@ void LogDDPSolver::resetFilter(CDDP &context) {
343331
// Evaluate log-barrier cost (includes path constraints)
344332
context.merit_function_ = context.cost_;
345333
constraint_violation_ = 0.0;
346-
double defect_violation = 0.0;
347334

348335
const auto &constraint_set = context.getConstraintSet();
349336

@@ -364,12 +351,7 @@ void LogDDPSolver::resetFilter(CDDP &context) {
364351
}
365352
}
366353
}
367-
368-
// Add defect violation penalty
369-
Eigen::VectorXd d = F_[t] - context.X_[t + 1];
370-
defect_violation += d.lpNorm<1>();
371354
}
372-
constraint_violation_ += defect_violation;
373355
context.inf_pr_ = constraint_violation_;
374356
}
375357

@@ -502,8 +484,6 @@ bool LogDDPSolver::backwardPass(CDDP &context) {
502484
for (int t = horizon - 1; t >= 0; --t) {
503485
const Eigen::VectorXd &x = context.X_[t];
504486
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
507487

508488
// Use pre-computed continuous-time dynamics Jacobians
509489
const Eigen::MatrixXd &Fx = F_x_[t];
@@ -517,8 +497,8 @@ bool LogDDPSolver::backwardPass(CDDP &context) {
517497
auto [l_xx, l_uu, l_ux] =
518498
context.getObjective().getRunningCostHessians(x, u, t);
519499

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);
500+
Eigen::VectorXd Q_x = l_x + A.transpose() * V_x;
501+
Eigen::VectorXd Q_u = l_u + B.transpose() * V_x;
522502
Eigen::MatrixXd Q_xx = l_xx + A.transpose() * V_xx * A;
523503
Eigen::MatrixXd Q_ux = l_ux + B.transpose() * V_xx * A;
524504
Eigen::MatrixXd Q_uu = l_uu + B.transpose() * V_xx * B;
@@ -631,61 +611,22 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
631611
result.control_trajectory = context.U_;
632612
result.state_trajectory[0] = context.getInitialState();
633613

634-
std::vector<Eigen::VectorXd> F_new = F_;
635-
636614
double cost_new = 0.0;
637615
double merit_function_new = 0.0;
638616
double rp_err = 0.0;
639-
double rf_err = 0.0;
640617

641-
// Rollout loop with multi-shooting logic from original
618+
// Single-shooting rollout
642619
for (int t = 0; t < horizon; ++t) {
643620
const Eigen::VectorXd delta_x_t =
644621
result.state_trajectory[t] - context.X_[t];
645622

646-
// Update control
647623
result.control_trajectory[t] =
648624
context.U_[t] + alpha * k_u_[t] + K_u_[t] * delta_x_t;
649625

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-
}
626+
result.state_trajectory[t + 1] = context.getSystem().getDiscreteDynamics(
627+
result.state_trajectory[t], result.control_trajectory[t],
628+
t * context.getTimestep());
687629

688-
// Robustness check
689630
if (!result.state_trajectory[t + 1].allFinite() ||
690631
!result.control_trajectory[t].allFinite()) {
691632
if (options.debug) {
@@ -698,13 +639,12 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
698639
}
699640
}
700641

701-
// Cost computation and filter line-search from original
642+
// Cost computation and constraint evaluation
702643
for (int t = 0; t < horizon; ++t) {
703644
cost_new += context.getObjective().running_cost(
704645
result.state_trajectory[t], result.control_trajectory[t], t);
705646

706647
for (const auto &constraint_pair : constraint_set) {
707-
const std::string &constraint_name = constraint_pair.first;
708648
Eigen::VectorXd g_t =
709649
constraint_pair.second->evaluate(result.state_trajectory[t],
710650
result.control_trajectory[t]) -
@@ -719,18 +659,15 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
719659
}
720660
}
721661
}
722-
723-
Eigen::VectorXd d = F_new[t] - result.state_trajectory[t + 1];
724-
rf_err += d.lpNorm<1>();
725662
}
726663

727664
cost_new +=
728665
context.getObjective().terminal_cost(result.state_trajectory.back());
729666
merit_function_new += cost_new;
730667

731-
// Filter-based acceptance using original logic with new options structure
668+
// Filter-based acceptance
732669
double constraint_violation_old = constraint_violation_;
733-
double constraint_violation_new = rf_err + rp_err;
670+
double constraint_violation_new = rp_err;
734671
double merit_function_old = context.merit_function_;
735672
bool filter_acceptance = false;
736673
double expected_improvement = alpha * dV_(0);
@@ -766,7 +703,6 @@ ForwardPassResult LogDDPSolver::forwardPass(CDDP &context, double alpha) {
766703
result.cost = cost_new;
767704
result.merit_function = merit_function_new;
768705
result.constraint_violation = constraint_violation_new;
769-
result.dynamics_trajectory = F_new;
770706
}
771707

772708
return result;

0 commit comments

Comments
 (0)