Skip to content

Commit b4b0b1f

Browse files
authored
Merge pull request #244 from Simple-Robotics/topic/nonmonotone-ls
Switch to nonmonotone linesearch
2 parents b49e460 + 77e2a49 commit b4b0b1f

29 files changed

+739
-494
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Add a multibody friction cone cost ([#234](https://github.com/Simple-Robotics/aligator/pull/234))
1313
- Add a `GravityCompensationResidual`, modelling $r(x,u) = Bu - G(q)$ ([#235](https://github.com/Simple-Robotics/aligator/pull/235))
1414
- Add Pixi support ([#240](https://github.com/Simple-Robotics/aligator/pull/240))
15+
- Added a nonmonotone linesearch procedure ([#244](https://github.com/Simple-Robotics/aligator/pull/244))
16+
- Add enum value `StepAcceptanceStrategy::LINESEARCH_NONMONOTONE` ([#244](https://github.com/Simple-Robotics/aligator/pull/244))
1517

1618
### Changed
1719

20+
- **API BREAKING:** Change enum value `StepAcceptanceStrategy::LINESEARCH` to `LINESEARCH_NONMONOTONE` ([#244](https://github.com/Simple-Robotics/aligator/pull/244))
21+
- Add constructor argument `StepAcceptanceStrategy sa_strategy`, defaults to nonmonotone
22+
- The minimum required version of proxsuite-nlp is now 0.10.0 ([#244](https://github.com/Simple-Robotics/aligator/pull/244))
1823
- `SolverProxDDP`: add temporary vectors for linesearch
1924
- `SolverProxDDP`: remove exceptions from `computeMultipliers()`, return a bool flag
25+
- HistoryCallback: take solver instance as argument
2026
- `gar`: rework and move sparse matrix utilities to `gar/utils.hpp`
2127
- `SolverProxDDP`: Rename `maxRefinementSteps_` and `refinementThreshold_` to snake-case
2228
- `SolverProxDDP`: make `linesearch_` public

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ set(BOOST_REQUIRED_COMPONENTS filesystem)
216216
set_boost_default_options()
217217
export_boost_default_options()
218218
add_project_dependency(Boost REQUIRED COMPONENTS ${BOOST_REQUIRED_COMPONENTS})
219-
add_project_dependency(proxsuite-nlp 0.8.0 REQUIRED PKG_CONFIG_REQUIRES "proxsuite-nlp >= 0.8.0")
219+
add_project_dependency(proxsuite-nlp 0.10.0 REQUIRED PKG_CONFIG_REQUIRES "proxsuite-nlp >= 0.10.0")
220220

221221
if(BUILD_WITH_PINOCCHIO_SUPPORT)
222222
message(STATUS "Building aligator with Pinocchio support.")

bindings/python/aligator/utils/plotting.py

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
from aligator import HistoryCallback, Results
55

66

7-
def plot_convergence(cb: HistoryCallback, ax: plt.Axes, res: Results = None):
7+
def plot_convergence(
8+
cb: HistoryCallback,
9+
ax: plt.Axes,
10+
res: Results = None,
11+
*,
12+
show_al_iters=False,
13+
legend_kwargs={},
14+
):
815
from proxsuite_nlp.utils import plot_pd_errs
916

1017
prim_infeas = cb.prim_infeas.tolist()
@@ -14,7 +21,28 @@ def plot_convergence(cb: HistoryCallback, ax: plt.Axes, res: Results = None):
1421
dual_infeas.append(res.dual_infeas)
1522
plot_pd_errs(ax, prim_infeas, dual_infeas)
1623
ax.grid(axis="y", which="major")
17-
return
24+
handles, labels = ax.get_legend_handles_labels()
25+
labels += [
26+
"Prim. err $p$",
27+
"Dual err $d$",
28+
]
29+
if show_al_iters:
30+
prim_tols = np.array(cb.prim_tols)
31+
al_iters = np.array(cb.al_index)
32+
labels.append("$\\eta_k$")
33+
34+
itrange = np.arange(len(al_iters))
35+
if itrange.size > 0:
36+
if al_iters.max() > 0:
37+
labels.append("AL iters")
38+
ax.step(itrange, prim_tols, c="green", alpha=0.9, lw=1.1)
39+
al_change = al_iters[1:] - al_iters[:-1]
40+
al_change_idx = itrange[:-1][al_change > 0]
41+
42+
ax.vlines(al_change_idx, *ax.get_ylim(), colors="gray", lw=4.0, alpha=0.5)
43+
44+
ax.legend(labels=labels, **legend_kwargs)
45+
return labels
1846

1947

2048
def plot_se2_pose(
@@ -50,14 +78,17 @@ def plot_controls_traj(
5078
joint_names=None,
5179
rmodel=None,
5280
figsize=(6.4, 6.4),
81+
xlabel="Time (s)",
5382
) -> tuple[plt.Figure, list[plt.Axes]]:
5483
t0 = times[0]
5584
tf = times[-1]
5685
us = np.asarray(us)
5786
nu = us.shape[1]
5887
nrows, r = divmod(nu, ncols)
5988
nrows += int(r > 0)
60-
if axes is None:
89+
90+
make_new_plot = axes is None
91+
if make_new_plot:
6192
fig, axes = plt.subplots(nrows, ncols, sharex="col", figsize=figsize)
6293
else:
6394
fig = axes.flat[0].get_figure()
@@ -77,9 +108,13 @@ def plot_controls_traj(
77108
ax.set_ylim(*ylim)
78109
if joint_names is not None:
79110
joint_name = joint_names[i].lower()
80-
ax.set_ylabel(joint_name)
81-
fig.supxlabel("Time $t$")
82-
fig.suptitle("Control trajectories")
111+
ax.set_title(joint_name, fontsize=8)
112+
if nu > 1:
113+
fig.supxlabel(xlabel)
114+
fig.suptitle("Control trajectories")
115+
else:
116+
axes[0].set_xlabel(xlabel)
117+
axes[0].set_title("Control trajectories")
83118
fig.tight_layout()
84119
return fig, axes
85120

@@ -92,6 +127,7 @@ def plot_velocity_traj(
92127
ncols=2,
93128
vel_limit=None,
94129
figsize=(6.4, 6.4),
130+
xlabel="Time (s)",
95131
) -> tuple[plt.Figure, list[plt.Axes]]:
96132
vs = np.asarray(vs)
97133
nv = rmodel.nv
@@ -111,7 +147,7 @@ def plot_velocity_traj(
111147
tf = times[-1]
112148

113149
if axes is None:
114-
fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
150+
fig, axes = plt.subplots(nrows, ncols, sharex=True, figsize=figsize)
115151
fig: plt.Figure
116152
else:
117153
fig = axes.flat[0].get_figure()
@@ -127,9 +163,9 @@ def plot_velocity_traj(
127163
ax.hlines(-vel_limit[i], t0, tf, colors="k", linestyles="--")
128164
ax.hlines(+vel_limit[i], t0, tf, colors="r", linestyles="dashdot")
129165
ax.set_ylim(*ylim)
130-
ax.set_ylabel(joint_name)
166+
ax.set_title(joint_name, fontsize=8)
131167

132-
fig.supxlabel("Time $t$")
168+
fig.supxlabel(xlabel)
133169
fig.suptitle("Velocity trajectories")
134170
fig.tight_layout()
135171
return fig, axes

bindings/python/src/expose-callbacks.cpp

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@
22
#include "aligator/python/callbacks.hpp"
33
#include "aligator/helpers/history-callback.hpp"
44

5+
#include "aligator/solvers/proxddp/solver-proxddp.hpp"
6+
#include "aligator/solvers/fddp/solver-fddp.hpp"
7+
58
namespace aligator {
69
namespace python {
710

811
using context::Scalar;
12+
using context::SolverFDDP;
13+
using context::SolverProxDDP;
14+
using HistoryCallback = HistoryCallbackTpl<Scalar>;
15+
16+
#define ctor(Solver) \
17+
bp::init<Solver *, bool, bool>( \
18+
("self"_a, "solver", "store_pd_vars"_a = true, "store_values"_a = true))
919

1020
void exposeHistoryCallback() {
11-
using HistoryCallback = HistoryCallbackTpl<Scalar>;
1221

1322
bp::scope in_history =
1423
bp::class_<HistoryCallback, bp::bases<CallbackBase>>(
1524
"HistoryCallback", "Store the history of solver's variables.",
16-
bp::init<bool, bool, bool>((bp::arg("self"),
17-
bp::arg("store_pd_vars") = true,
18-
bp::arg("store_values") = true,
19-
bp::arg("store_residuals") = true)))
25+
bp::no_init)
26+
.def(ctor(SolverProxDDP))
27+
.def(ctor(SolverFDDP))
2028
#define _c(name) def_readonly(#name, &HistoryCallback::name)
2129
._c(xs)
2230
._c(us)
@@ -36,9 +44,8 @@ void exposeHistoryCallback() {
3644
void exposeCallbacks() {
3745
bp::register_ptr_to_python<shared_ptr<CallbackBase>>();
3846

39-
bp::class_<CallbackWrapper, boost::noncopyable>("BaseCallback",
40-
"Base callback for solvers.",
41-
bp::init<>(bp::args("self")))
47+
bp::class_<CallbackWrapper, boost::noncopyable>(
48+
"BaseCallback", "Base callback for solvers.", bp::init<>(("self"_a)))
4249
.def("call", bp::pure_virtual(&CallbackWrapper::call),
4350
bp::args("self", "workspace", "results"));
4451

bindings/python/src/expose-solver-prox.cpp

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "aligator/solvers/proxddp/solver-proxddp.hpp"
88

99
#include <eigenpy/std-unique-ptr.hpp>
10+
#include <eigenpy/variant.hpp>
1011

1112
namespace aligator {
1213
namespace python {
@@ -19,8 +20,8 @@ void exposeProxDDP() {
1920
using context::VectorRef;
2021
using context::Workspace;
2122

22-
eigenpy::register_symbolic_link_to_registered_type<
23-
Linesearch<Scalar>::Options>();
23+
using LsOptions = Linesearch<Scalar>::Options;
24+
eigenpy::register_symbolic_link_to_registered_type<LsOptions>();
2425
eigenpy::register_symbolic_link_to_registered_type<LinesearchStrategy>();
2526
eigenpy::register_symbolic_link_to_registered_type<
2627
proxsuite::nlp::LSInterpolation>();
@@ -74,6 +75,7 @@ void exposeProxDDP() {
7475
.def(PrintableVisitor<Results>());
7576

7677
using SolverType = SolverProxDDPTpl<Scalar>;
78+
using ls_variant_t = SolverType::LinesearchVariant::variant_t;
7779

7880
auto cls =
7981
bp::class_<SolverType, boost::noncopyable>(
@@ -84,9 +86,10 @@ void exposeProxDDP() {
8486
" The solver instance initializes both a Workspace and a Results "
8587
"struct.",
8688
bp::init<const Scalar, const Scalar, std::size_t, VerboseLevel,
87-
HessianApprox>(
89+
StepAcceptanceStrategy, HessianApprox>(
8890
("self"_a, "tol", "mu_init"_a = 1e-2, "max_iters"_a = 1000,
8991
"verbose"_a = VerboseLevel::QUIET,
92+
"sa_strategy"_a = StepAcceptanceStrategy::LINESEARCH_NONMONOTONE,
9093
"hess_approx"_a = HessianApprox::GAUSS_NEWTON)))
9194
.def("cycleProblem", &SolverType::cycleProblem,
9295
("self"_a, "problem", "data"),
@@ -110,7 +113,7 @@ void exposeProxDDP() {
110113
.def_readwrite("max_al_iters", &SolverType::max_al_iters,
111114
"Maximum number of AL iterations.")
112115
.def_readwrite("ls_mode", &SolverType::ls_mode, "Linesearch mode.")
113-
.def_readwrite("sa_strategy", &SolverType::sa_strategy,
116+
.def_readwrite("sa_strategy", &SolverType::sa_strategy_,
114117
"StepAcceptance strategy.")
115118
.def_readwrite("rollout_type", &SolverType::rollout_type_,
116119
"Rollout type.")
@@ -120,7 +123,6 @@ void exposeProxDDP() {
120123
"Minimum regularization value.")
121124
.def_readwrite("reg_max", &SolverType::reg_max,
122125
"Maximum regularization value.")
123-
.def_readwrite("lq_print_detailed", &SolverType::lq_print_detailed)
124126
.def("updateLQSubproblem", &SolverType::updateLQSubproblem, "self"_a)
125127
.def("computeCriterion", &SolverType::computeCriterion, "self"_a,
126128
"Compute problem stationarity.")
@@ -143,13 +145,25 @@ void exposeProxDDP() {
143145
"(target_tol) will not be synced when the latter changes and "
144146
"`solver.run()` is called.")
145147
.def(SolverVisitor<SolverType>())
148+
.add_property("linesearch",
149+
bp::make_function(
150+
+[](const SolverType &s) -> const ls_variant_t & {
151+
return s.linesearch_;
152+
},
153+
eigenpy::ReturnInternalVariant<ls_variant_t>{}))
146154
.def("run", &SolverType::run,
147155
("self"_a, "problem", "xs_init"_a = bp::list(),
148156
"us_init"_a = bp::list(), "vs_init"_a = bp::list(),
149157
"lams_init"_a = bp::list()),
150158
"Run the algorithm. Can receive initial guess for "
151159
"multiplier trajectory.");
152160

161+
bp::class_<NonmonotoneLinesearch<Scalar>, bp::bases<Linesearch<Scalar>>>(
162+
"NonmonotoneLinesearch", bp::no_init)
163+
.def(bp::init<LsOptions>(("self"_a, "options")))
164+
.def_readwrite("avg_eta", &NonmonotoneLinesearch<Scalar>::avg_eta)
165+
.def_readwrite("beta_dec", &NonmonotoneLinesearch<Scalar>::beta_dec);
166+
153167
{
154168
using AlmParams = SolverType::AlmParams;
155169
bp::scope scope{cls};

bindings/python/src/module.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ static void exposeEnums() {
4242

4343
bp::enum_<StepAcceptanceStrategy>("StepAcceptanceStrategy",
4444
"Step acceptance strategy.")
45-
.value("SA_LINESEARCH", StepAcceptanceStrategy::LINESEARCH)
45+
.value("SA_LINESEARCH_ARMIJO", StepAcceptanceStrategy::LINESEARCH_ARMIJO)
46+
.value("SA_LINESEARCH_NONMONOTONE",
47+
StepAcceptanceStrategy::LINESEARCH_NONMONOTONE)
4648
.value("SA_FILTER", StepAcceptanceStrategy::FILTER)
4749
.export_values();
4850
}

examples/acrobot.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ class Args(ArgsBase):
7070
)
7171

7272
tol = 1e-3
73-
mu_init = 10.0
73+
mu_init = 1e-2
7474
solver = aligator.SolverProxDDP(tol, mu_init=mu_init, verbose=aligator.VERBOSE)
7575
solver.max_iters = 200
76-
solver.rollout_type = aligator.ROLLOUT_LINEAR
76+
solver.rollout_type = aligator.ROLLOUT_NONLINEAR
7777
solver.linear_solver_choice = aligator.LQ_SOLVER_STAGEDENSE
7878
solver.setup(problem)
7979

@@ -96,23 +96,28 @@ class Args(ArgsBase):
9696
from aligator.utils.plotting import plot_controls_traj
9797

9898
times = np.linspace(0, Tf, nsteps + 1)
99-
fig1 = plot_controls_traj(times, res.us, ncols=1, rmodel=rmodel, figsize=(6.4, 3.2))
99+
fig1, axes = plot_controls_traj(
100+
times, res.us, ncols=1, rmodel=rmodel, figsize=(6.4, 3.2)
101+
)
102+
plt.title("Controls (N/m)")
100103
fig1.tight_layout()
101104
xs = np.stack(res.xs)
102105
vs = xs[:, nq:]
103106

104107
theta_s = np.zeros((nsteps + 1, 2))
105108
theta_s0 = space.difference(space.neutral(), x0)[:2]
106109
theta_s = theta_s0 + np.cumsum(vs * timestep, axis=0)
107-
fig2 = plt.figure(figsize=(6.4, 6.4))
108-
plt.subplot(211)
110+
fig2 = plt.figure(figsize=(6.4, 3.2))
111+
plt.subplot(1, 2, 1)
109112
plt.plot(times, theta_s, label=("$\\theta_0$", "$\\theta_1$"))
110-
plt.title("Joint angles")
113+
plt.title("Joint angles (rad)")
114+
plt.xlabel("Time (s)")
111115
plt.legend()
112-
plt.subplot(212)
116+
plt.subplot(1, 2, 2)
113117
plt.plot(times, xs[:, nq:], label=("$\\dot\\theta_0$", "$\\dot\\theta_1$"))
114118
plt.legend()
115-
plt.title("Joint velocities")
119+
plt.title("Joint velocities (rad/s)")
120+
plt.xlabel("Time (s)")
116121
fig2.tight_layout()
117122

118123
_fig_dict = {"controls": fig1, "velocities": fig2}

0 commit comments

Comments
 (0)