Skip to content

Commit ad294b4

Browse files
authored
Merge pull request #348 from Simple-Robotics/topic/simplify-solver
Revamp ProxDDP solver : remove proximal iteration over co-state, temporarily remove implicit dynamics
2 parents 9dbbeee + 6f261a7 commit ad294b4

File tree

199 files changed

+4289
-7262
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

199 files changed

+4289
-7262
lines changed

.github/workflows/macos-linux-pixi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
fail-fast: false
4040
matrix:
4141
os: [ubuntu-latest, macos-latest, macos-13]
42-
environment: [all, all-py39]
42+
environment: [all, all-py310]
4343
build_type: [Release, Debug]
4444

4545
steps:

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
This release brings a major change to the ProxDDP solver, which no longer uses proximal iteration the co-state (the dynamics' Lagrange multiplies). This choice is made to increase the solver's overall performance.
11+
12+
The ProxDDP solver now defaults to using a linear rollout for the state-control trajectory update.
13+
14+
Furthermore, on a temporary basis, implicit discrete dynamics **are no longer supported** in the API and solvers.
15+
1016
### Fixed
1117

1218
- gar : fix missing move assignment operator in `LqrProblemTpl`
1319
- Fix C++20 support. Fix `consteval` compilation errors related to fmt
20+
- Fix `aligator::gar::ParallelRiccatiSolver` missing from docs
21+
- Fix missing set of Pinocchio-support (cost, dynamics, etc) classes
1422

1523
### Changed
1624

25+
- gar : allow setting number of refinement iterations for condensed KKT solver
26+
- ProxDDP solver : change default rollout type to `RolloutType::LINEAR`/`ROLLOUT_LINEAR`
27+
- solvers/proxddp : make `Workspace` an allocator-aware class
28+
- core/manifold-base : mark some functions `[[nodiscard]]`
1729
- readme/cmake : update actually expected minimum version of eigenpy to 3.9
1830
- solvers : make proxddp algo's Results class copyable again (in C++ and Python) (https://github.com/Simple-Robotics/aligator/pull/322)
1931
- python/visitors : also set `__copy__` method on exposed class with `CopyableVisitor` (https://github.com/Simple-Robotics/aligator/pull/322)
2032
- python : make `Results` copyable (using `CopyableVisitor`) (https://github.com/Simple-Robotics/aligator/pull/322)
33+
- ProxDDP solver : remove proximal iteration/ALM over co-states
34+
- ProxDDP solver : throw when given invalid linesearch enum value
35+
- core/linesearches : move `LinesearchOptions` struct out of the `Linesearch` template class, add CTAD
36+
- Change all tests to use Catch2 instead of Boost.Test
37+
- The Riccati algorithms now run faster after the dual-regularisation on co-states and the QR for the implicit dynamics have been removed. The algos might be less numerically accurate.
38+
39+
#### Changes to dynamics
40+
41+
- Remove explicit dynamics (incl. explicit integrators) from `DynamicsModel` class hierarchy
42+
- Make `ExplicitDynamicsModel` used everywhere in API (e.g. `StageModel` now takes/stores `polymorphic<ExplicitDynamicsModel>`)
43+
- Directly store state space repr dim and actual dim (`nx` and `ndx`) in `ManifoldAbstract` class
2144

2245
### Added
2346

2447
- gar: add CTAD for the `ParallelRiccatiSolver` and `ProximalRiccatiSolver` classes
2548
- testing: added a test_mpc.py script to test parallel and serial mpc implementations (https://github.com/Simple-Robotics/aligator/pull/331)
2649
- modelling : added wheeled inverted pendulum dynamics (https://github.com/Simple-Robotics/aligator/pull/326)
2750
- CMake option to `BUILD_STANDALONE_PYTHON_INTERFACE` (https://github.com/Simple-Robotics/aligator/pull/347)
51+
- utils: add free function `forwardDynamics()` (replaces previous struct) in `aligator/utils/forward-dyn.hpp`
52+
53+
### Removed
54+
55+
- fwd.hpp : remove deprecated typedef `ODEDataTpl`
56+
- gar: remove CHOLDMOD backend (https://github.com/Simple-Robotics/aligator/pull/345)
57+
- gar: remove support for implicit dynamics in LQ solver interface
58+
- remove member `LqrKnotTpl::E`
59+
- simplify Riccati kernel algorithm
2860

2961
## [0.15.0] - 2025-05-23
3062

CMakeLists.txt

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,6 @@ cmake_dependent_option(
149149
OFF
150150
)
151151
option(BUILD_WITH_OPENMP_SUPPORT "Build the library with the OpenMP support" ON)
152-
option(
153-
BUILD_WITH_CHOLMOD_SUPPORT
154-
"Build with support for backends enabled by Cholmod"
155-
OFF
156-
)
157152
option(BUILD_BENCHMARKS "Build benchmarks" ON)
158153
option(BUILD_EXAMPLES "Build examples" ON)
159154
cmake_dependent_option(
@@ -236,19 +231,6 @@ if(BUILD_CROCODDYL_COMPAT)
236231
list(APPEND CFLAGS_DEPENDENCIES "-DALIGATOR_WITH_CROCODDYL_COMPAT")
237232
endif()
238233

239-
if(BUILD_WITH_CHOLMOD_SUPPORT)
240-
set(
241-
CMAKE_MODULE_PATH
242-
${JRL_CMAKE_MODULES}/find-external/CHOLMOD
243-
${CMAKE_MODULE_PATH}
244-
)
245-
ADD_PROJECT_DEPENDENCY(CHOLMOD REQUIRED)
246-
message(
247-
STATUS
248-
"Build with CHOLMOD support (LGPL). See CHOLMOD/Doc/License.txt for further details."
249-
)
250-
endif()
251-
252234
if(BUILD_PYTHON_INTERFACE)
253235
set(PYTHON_COMPONENTS Interpreter Development NumPy)
254236
ADD_PROJECT_DEPENDENCY(
@@ -372,11 +354,6 @@ function(create_library)
372354
target_compile_definitions(${PROJECT_NAME} PUBLIC ALIGATOR_WITH_PINOCCHIO)
373355
endif()
374356

375-
if(BUILD_WITH_CHOLMOD_SUPPORT)
376-
target_link_libraries(${PROJECT_NAME} PUBLIC CHOLMOD::CHOLMOD)
377-
target_compile_definitions(${PROJECT_NAME} PUBLIC ALIGATOR_WITH_CHOLMOD)
378-
endif()
379-
380357
if(BUILD_WITH_OPENMP_SUPPORT)
381358
target_link_libraries(${PROJECT_NAME} PUBLIC OpenMP::OpenMP_CXX)
382359
endif()

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ cmake --build . -jNCPUS
7575
* (optional) [Pinocchio](https://github.com/stack-of-tasks/pinocchio) | [conda](https://anaconda.org/conda-forge/pinocchio)
7676
* (optional) [Crocoddyl](https://github.com/loco-3d/crocoddyl) | [conda](https://anaconda.org/conda-forge/crocoddyl)
7777
* (optional) [example-robot-data](https://github.com/Gepetto/example-robot-data) | [conda](https://anaconda.org/conda-forge/example-robot-data) (required for some examples and benchmarks)
78+
* (optional) [Catch2](https://github.com/catchorg/Catch2) | [conda](https://anaconda.org/conda-forge/catch2) for testing
7879
* a C++17 compliant compiler
7980

8081
#### Python dependencies

bench/gar-riccati.cpp

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,50 @@
1515

1616
using namespace aligator::gar;
1717

18-
const uint nx = 36;
19-
const uint nu = 12;
18+
static constexpr uint nx = 36;
19+
static constexpr uint nu = 12;
20+
static constexpr uint nc = 32;
21+
static constexpr double mueq = 1e-11;
22+
static std::mt19937 rng;
23+
static normal_unary_op normal_op{rng};
2024

2125
static void BM_serial(benchmark::State &state) {
2226
uint horz = (uint)state.range(0);
23-
VectorXs x0 = VectorXs::NullaryExpr(nx, normal_unary_op{});
24-
const LqrProblemTpl<double> problem = generate_problem(x0, horz, nx, nu);
27+
VectorXs x0 = VectorXs::NullaryExpr(nx, normal_op);
28+
const LqrProblemTpl<double> problem =
29+
generateLqProblem(rng, x0, horz, nx, nu, 0, nc);
2530
ProximalRiccatiSolver<double> solver(problem);
26-
const double mu = 1e-11;
2731
auto [xs, us, vs, lbdas] = lqrInitializeSolution(problem);
2832
for (auto _ : state) {
29-
solver.backward(mu, mu);
33+
solver.backward(mueq);
3034
solver.forward(xs, us, vs, lbdas);
3135
}
3236
}
3337

3438
#ifdef ALIGATOR_MULTITHREADING
3539
template <uint NPROC> static void BM_parallel(benchmark::State &state) {
3640
uint horz = (uint)state.range(0);
37-
VectorXs x0 = VectorXs::NullaryExpr(nx, normal_unary_op{});
38-
LqrProblemTpl<double> problem = generate_problem(x0, horz, nx, nu);
41+
VectorXs x0 = VectorXs::NullaryExpr(nx, normal_op);
42+
LqrProblemTpl<double> problem =
43+
generateLqProblem(rng, x0, horz, nx, nu, 0, nc);
3944
ParallelRiccatiSolver<double> solver(problem, NPROC);
40-
const double mu = 1e-11;
4145
auto [xs, us, vs, lbdas] = lqrInitializeSolution(problem);
4246
for (auto _ : state) {
43-
solver.backward(mu, mu);
47+
solver.backward(mueq);
4448
solver.forward(xs, us, vs, lbdas);
4549
}
4650
}
4751
#endif
4852

4953
static void BM_stagedense(benchmark::State &state) {
5054
uint horz = (uint)state.range(0);
51-
VectorXs x0 = VectorXs::NullaryExpr(nx, normal_unary_op{});
52-
LqrProblemTpl<double> problem = generate_problem(x0, horz, nx, nu);
55+
VectorXs x0 = VectorXs::NullaryExpr(nx, normal_op);
56+
LqrProblemTpl<double> problem =
57+
generateLqProblem(rng, x0, horz, nx, nu, 0, nc);
5358
RiccatiSolverDense<double> solver(problem);
54-
const double mu = 1e-11;
5559
auto [xs, us, vs, lbdas] = lqrInitializeSolution(problem);
5660
for (auto _ : state) {
57-
solver.backward(mu, mu);
61+
solver.backward(mueq);
5862
solver.forward(xs, us, vs, lbdas);
5963
}
6064
}

bench/lqr.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/// @file
22
/// @brief Linear-quadratic regulator
33

4+
#include "aligator/core/traj-opt-problem.hpp"
45
#include "aligator/solvers/proxddp/solver-proxddp.hpp"
56
#include "aligator/solvers/fddp/solver-fddp.hpp"
67
#include "aligator/utils/rollout.hpp"
78
#include "aligator/modelling/costs/quad-costs.hpp"
8-
99
#include "aligator/modelling/linear-discrete-dynamics.hpp"
1010

1111
#include <benchmark/benchmark.h>

bindings/python/aligator/utils/plotting.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,59 @@
33

44
from aligator import HistoryCallback, Results
55

6+
_ROOT_10 = 10.0**0.5
7+
8+
9+
def plot_pd_errs(ax0, prim_errs, dual_errs):
10+
import matplotlib.pyplot as plt
11+
12+
ax0: plt.Axes
13+
prim_errs = np.asarray(prim_errs)
14+
dual_errs = np.asarray(dual_errs)
15+
ax0.plot(prim_errs, c="tab:blue")
16+
ax0.set_xlabel("Iterations")
17+
col2 = "tab:orange"
18+
ax0.plot(dual_errs, c=col2)
19+
ax0.spines["top"].set_visible(False)
20+
ax0.spines["right"].set_color(col2)
21+
ax0.yaxis.label.set_color(col2)
22+
ax0.set_yscale("log")
23+
ax0.legend(["Primal error $p$", "Dual error $d$"])
24+
ax0.set_title("Solver primal-dual residuals")
25+
26+
# handle scaling
27+
yhigh = ax0.get_ylim()[1]
28+
if len(prim_errs) == 0 or len(dual_errs) == 0:
29+
return
30+
mach_eps = np.finfo(float).eps
31+
dmask = dual_errs > 2 * mach_eps
32+
pmask = prim_errs > 2 * mach_eps
33+
ymin = np.finfo(float).max
34+
if dmask.any():
35+
ymin = np.min(dual_errs[dmask])
36+
if pmask.any() and sum(prim_errs > 0) > 0:
37+
ymin = min(np.min(prim_errs[pmask]), ymin)
38+
ax0.set_ylim(ymin / _ROOT_10, yhigh)
39+
640

741
def plot_convergence(
842
cb: HistoryCallback,
943
ax: plt.Axes,
1044
res: Results = None,
1145
*,
1246
show_al_iters=False,
47+
target_tol: float = None,
1348
legend_kwargs={},
1449
):
15-
from proxsuite_nlp.utils import plot_pd_errs
16-
1750
prim_infeas = cb.prim_infeas.tolist()
1851
dual_infeas = cb.dual_infeas.tolist()
1952
if res is not None:
2053
prim_infeas.append(res.primal_infeas)
2154
dual_infeas.append(res.dual_infeas)
2255
plot_pd_errs(ax, prim_infeas, dual_infeas)
56+
2357
ax.grid(axis="y", which="major")
24-
handles, labels = ax.get_legend_handles_labels()
58+
_, labels = ax.get_legend_handles_labels()
2559
labels += [
2660
"Prim. err $p$",
2761
"Dual err $d$",
@@ -41,6 +75,9 @@ def plot_convergence(
4175

4276
ax.vlines(al_change_idx, *ax.get_ylim(), colors="gray", lw=4.0, alpha=0.5)
4377

78+
if target_tol:
79+
ax.axhline(target_tol, color="k", lw=1.2)
80+
4481
ax.legend(labels=labels, **legend_kwargs)
4582
return labels
4683

bindings/python/include/aligator/python/fwd.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#pragma once
22

3-
#include "aligator/fwd.hpp"
43
#include "aligator/context.hpp"
54

65
namespace boost {

bindings/python/include/aligator/python/modelling/explicit-dynamics.hpp

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// @file
2-
/// @copyright Copyright (C) 2022-2024 LAAS-CNRS, INRIA
2+
/// @copyright Copyright (C) 2022-2024 LAAS-CNRS, 2022-2025 INRIA
33
#pragma once
44

55
#include "aligator/python/fwd.hpp"
@@ -16,16 +16,16 @@ using context::DynamicsData;
1616
/// child virtual class (e.g. integrator classes).
1717
/// @tparam ExplicitBase The derived virtual class that is being exposed.
1818
/// @sa PyStageFunction
19-
template <class ExplicitBase = context::ExplicitDynamics>
19+
template <class Base = context::ExplicitDynamics>
2020
struct PyExplicitDynamics final
21-
: ExplicitBase,
22-
PolymorphicWrapper<PyExplicitDynamics<ExplicitBase>, ExplicitBase> {
21+
: Base,
22+
PolymorphicWrapper<PyExplicitDynamics<Base>, Base> {
2323
using Scalar = context::Scalar;
2424
ALIGATOR_DYNAMIC_TYPEDEFS(Scalar);
2525
// All functions in the interface take this type for output
2626
using Data = ExplicitDynamicsDataTpl<Scalar>;
2727

28-
using ExplicitBase::ExplicitBase;
28+
using Base::Base;
2929

3030
void forward(const ConstVectorRef &x, const ConstVectorRef &u,
3131
Data &data) const {
@@ -37,14 +37,11 @@ struct PyExplicitDynamics final
3737
ALIGATOR_PYTHON_OVERRIDE_PURE(void, "dForward", x, u, boost::ref(data));
3838
}
3939

40-
shared_ptr<DynamicsData> createData() const {
41-
ALIGATOR_PYTHON_OVERRIDE(shared_ptr<DynamicsData>, ExplicitBase,
42-
createData, );
40+
shared_ptr<Data> createData() const {
41+
ALIGATOR_PYTHON_OVERRIDE(shared_ptr<Data>, Base, createData, );
4342
}
4443

45-
shared_ptr<DynamicsData> default_createData() const {
46-
return ExplicitBase::createData();
47-
}
44+
shared_ptr<Data> default_createData() const { return Base::createData(); }
4845
};
4946

5047
} // namespace python

bindings/python/src/expose-cartesian-product.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ void exposeCartesianProduct() {
3838
("self"_a, "left", "right")))
3939
.def(
4040
"getComponent",
41-
+[](CartesianProduct const &m, std::size_t i) -> const Manifold & {
41+
+[](const CartesianProduct &m, std::size_t i) -> const Manifold & {
4242
if (i >= m.numComponents()) {
4343
PyErr_SetString(PyExc_IndexError, "Index out of bounds.");
4444
bp::throw_error_already_set();
@@ -47,8 +47,12 @@ void exposeCartesianProduct() {
4747
},
4848
bp::return_internal_reference<>(), ("self"_a, "i"),
4949
"Get the i-th component of the Cartesian product.")
50-
.def("addComponent", &CartesianProduct::addComponent<PolymorphicManifold>,
51-
("self"_a, "c"), "Add a component to the Cartesian product.")
50+
.def(
51+
"addComponent",
52+
+[](CartesianProduct &m, const PolymorphicManifold &c) {
53+
m.addComponent(c);
54+
},
55+
("self"_a, "c"), "Add a component to the Cartesian product.")
5256
.add_property("num_components", &CartesianProduct::numComponents,
5357
"Get the number of components in the Cartesian product.")
5458
.def(

0 commit comments

Comments
 (0)