Skip to content

Commit b08ab23

Browse files
authored
Implement RobustArmijo and refactor Armijo Line Search (#53)
* Implement RobustArmijo and refactor Armijo LS * Add documentation * Fix bug * Fix CMake deps order * Expose delta_relative_tolerance * Fix delta_relative_tolerance * Compute new_grad as needed * Change default LS to RobustArmijo * Enabled use_grad_norm in Backtracking
1 parent 25ebacb commit b08ab23

20 files changed

+240
-132
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ if(POLYSOLVE_LARGE_INDEX)
173173
endif()
174174

175175
target_compile_definitions(polysolve_linear PRIVATE POLYSOLVE_LINEAR_SPEC="${PROJECT_SOURCE_DIR}/linear-solver-spec.json")
176-
target_compile_definitions(polysolve PRIVATE POLYSOLVE_NON_LINEAR_SPEC="${PROJECT_SOURCE_DIR}/non-linear-solver-spec.json")
176+
target_compile_definitions(polysolve PRIVATE POLYSOLVE_NON_LINEAR_SPEC="${PROJECT_SOURCE_DIR}/nonlinear-solver-spec.json")
177177
target_compile_definitions(polysolve_linear PUBLIC POLYSOLVE_JSON_SPEC_DIR="${PROJECT_SOURCE_DIR}")
178178

179179

non-linear-solver-spec.json renamed to nonlinear-solver-spec.json

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
[
2-
{
1+
[{
32
"pointer": "/",
43
"default": null,
54
"type": "object",
@@ -277,17 +276,19 @@
277276
"max_step_size_iter_final",
278277
"default_init_step_size",
279278
"step_ratio",
280-
"Armijo"
279+
"Armijo",
280+
"RobustArmijo"
281281
],
282282
"doc": "Settings for line-search in the nonlinear solver"
283283
},
284284
{
285285
"pointer": "/line_search/method",
286-
"default": "Backtracking",
286+
"default": "RobustArmijo",
287287
"type": "string",
288288
"options": [
289289
"Armijo",
290290
"ArmijoAlt",
291+
"RobustArmijo",
291292
"Backtracking",
292293
"MoreThuente",
293294
"None"
@@ -347,10 +348,27 @@
347348
},
348349
{
349350
"pointer": "/line_search/Armijo/c",
350-
"default": 0.5,
351+
"default": 1e-4,
351352
"type": "float",
353+
"min_value": 0,
352354
"doc": "Armijo c parameter."
353355
},
356+
{
357+
"pointer": "/line_search/RobustArmijo",
358+
"default": null,
359+
"type": "object",
360+
"optional": [
361+
"delta_relative_tolerance"
362+
],
363+
"doc": "Options for RobustArmijo."
364+
},
365+
{
366+
"pointer": "/line_search/RobustArmijo/delta_relative_tolerance",
367+
"default": 0.1,
368+
"type": "float",
369+
"min_value": 0,
370+
"doc": "Relative tolerance on E to switch to approximate."
371+
},
354372
{
355373
"pointer": "/box_constraints",
356374
"type": "object",

src/polysolve/nonlinear/Solver.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ namespace polysolve::nonlinear
216216
const auto g_norm_tol = this->m_stop.gradNorm;
217217
this->m_stop.gradNorm = first_grad_norm_tol;
218218

219-
StopWatch stop_watch("non-linear solver", this->total_time, m_logger);
219+
StopWatch stop_watch("nonlinear solver", this->total_time, m_logger);
220220
stop_watch.start();
221221

222222
m_logger.debug(

src/polysolve/nonlinear/descent_strategies/Newton.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ namespace polysolve::nonlinear
104104
const double characteristic_length,
105105
spdlog::logger &logger);
106106

107-
std::string name() const override { return internal_name() + "RegularizedNewton"; }
107+
std::string name() const override
108+
{
109+
return fmt::format("{}RegularizedNewton (reg_weight={:g})", internal_name(), reg_weight);
110+
}
108111

109112
void reset(const int ndof) override;
110113
bool handle_error() override;

src/polysolve/nonlinear/line_search/Armijo.cpp

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22

33
#include "Armijo.hpp"
44

5-
#include <polysolve/Utils.hpp>
6-
7-
#include <spdlog/spdlog.h>
8-
95
namespace polysolve::nonlinear::line_search
106
{
117
Armijo::Armijo(const json &params, spdlog::logger &logger)
@@ -14,58 +10,25 @@ namespace polysolve::nonlinear::line_search
1410
c = params["line_search"]["Armijo"]["c"];
1511
}
1612

17-
double Armijo::compute_descent_step_size(
18-
const TVector &x,
19-
const TVector &searchDir,
13+
void Armijo::init_compute_descent_step_size(
14+
const TVector &delta_x,
15+
const TVector &old_grad)
16+
{
17+
armijo_criteria = c * delta_x.dot(old_grad);
18+
assert(armijo_criteria <= 0);
19+
}
20+
21+
bool Armijo::criteria(
22+
const TVector &delta_x,
2023
Problem &objFunc,
2124
const bool use_grad_norm,
22-
const double old_energy_in,
23-
const double starting_step_size)
25+
const double old_energy,
26+
const TVector &old_grad,
27+
const TVector &new_x,
28+
const double new_energy,
29+
const double step_size) const
2430
{
25-
TVector grad(x.rows());
26-
double f_in = old_energy_in;
27-
double alpha = starting_step_size;
28-
29-
bool valid;
30-
31-
TVector x1 = x + alpha * searchDir;
32-
{
33-
POLYSOLVE_SCOPED_STOPWATCH("constraint set update in LS", constraint_set_update_time, m_logger);
34-
objFunc.solution_changed(x1);
35-
}
36-
37-
objFunc.gradient(x, grad);
38-
39-
double f = use_grad_norm ? grad.squaredNorm() : objFunc.value(x1);
40-
const double cache = c * grad.dot(searchDir);
41-
valid = objFunc.is_step_valid(x, x1);
42-
43-
while ((std::isinf(f) || std::isnan(f) || f > f_in + alpha * cache || !valid) && alpha > current_min_step_size() && cur_iter <= current_max_step_size_iter())
44-
{
45-
alpha *= step_ratio;
46-
x1 = x + alpha * searchDir;
47-
48-
{
49-
POLYSOLVE_SCOPED_STOPWATCH("constraint set update in LS", constraint_set_update_time, m_logger);
50-
objFunc.solution_changed(x1);
51-
}
52-
53-
if (use_grad_norm)
54-
{
55-
objFunc.gradient(x1, grad);
56-
f = grad.squaredNorm();
57-
}
58-
else
59-
f = objFunc.value(x1);
60-
61-
valid = objFunc.is_step_valid(x, x1);
62-
63-
m_logger.trace("ls it: {} f: {} (f_in + alpha * Cache): {} invalid: {} ", cur_iter, f, f_in + alpha * cache, !valid);
64-
65-
cur_iter++;
66-
}
67-
68-
return alpha;
31+
return new_energy <= old_energy + step_size * armijo_criteria;
6932
}
7033

7134
}; // namespace polysolve::nonlinear::line_search
Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#pragma once
22

3-
#include "LineSearch.hpp"
3+
#include "Backtracking.hpp"
44

55
namespace polysolve::nonlinear::line_search
66
{
7-
class Armijo : public LineSearch
7+
class Armijo : public Backtracking
88
{
99
public:
10-
using Superclass = LineSearch;
10+
using Superclass = Backtracking;
1111
using typename Superclass::Scalar;
1212
using typename Superclass::TVector;
1313

@@ -16,15 +16,21 @@ namespace polysolve::nonlinear::line_search
1616
virtual std::string name() override { return "Armijo"; }
1717

1818
protected:
19-
double compute_descent_step_size(
20-
const TVector &x,
19+
virtual void init_compute_descent_step_size(
20+
const TVector &delta_x,
21+
const TVector &old_grad) override;
22+
23+
virtual bool criteria(
2124
const TVector &delta_x,
2225
Problem &objFunc,
2326
const bool use_grad_norm,
24-
const double old_energy_in,
25-
const double starting_step_size) override;
27+
const double old_energy,
28+
const TVector &old_grad,
29+
const TVector &new_x,
30+
const double new_energy,
31+
const double step_size) const override;
2632

27-
private:
2833
double c;
34+
double armijo_criteria; ///< cached value: c * delta_x.dot(old_grad)
2935
};
3036
} // namespace polysolve::nonlinear::line_search

src/polysolve/nonlinear/line_search/Backtracking.cpp

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
#include <spdlog/spdlog.h>
66

7-
#include <cfenv>
8-
97
namespace polysolve::nonlinear::line_search
108
{
119

@@ -20,58 +18,68 @@ namespace polysolve::nonlinear::line_search
2018
Problem &objFunc,
2119
const bool use_grad_norm,
2220
const double old_energy,
21+
const TVector &old_grad,
2322
const double starting_step_size)
2423
{
2524
double step_size = starting_step_size;
2625

27-
TVector grad(x.rows());
26+
init_compute_descent_step_size(delta_x, old_grad);
2827

29-
// Find step that reduces the energy
30-
double cur_energy = std::nan("");
31-
bool is_step_valid = false;
32-
while (step_size > current_min_step_size() && cur_iter < current_max_step_size_iter())
28+
for (; step_size > current_min_step_size() && cur_iter < current_max_step_size_iter(); step_size *= step_ratio, ++cur_iter)
3329
{
34-
TVector new_x = x + step_size * delta_x;
30+
const TVector new_x = x + step_size * delta_x;
3531

3632
try
3733
{
38-
POLYSOLVE_SCOPED_STOPWATCH("solution changed - constraint set update in LS", this->constraint_set_update_time, m_logger);
34+
POLYSOLVE_SCOPED_STOPWATCH("solution changed - constraint set update in LS", constraint_set_update_time, m_logger);
3935
objFunc.solution_changed(new_x);
4036
}
4137
catch (const std::runtime_error &e)
4238
{
4339
m_logger.warn("Failed to take step due to \"{}\", reduce step size...", e.what());
44-
45-
step_size *= step_ratio;
46-
this->cur_iter++;
4740
continue;
4841
}
4942

50-
if (use_grad_norm)
43+
if (!objFunc.is_step_valid(x, new_x))
5144
{
52-
objFunc.gradient(new_x, grad);
53-
cur_energy = grad.squaredNorm();
45+
continue;
5446
}
55-
else
56-
cur_energy = objFunc.value(new_x);
5747

58-
is_step_valid = objFunc.is_step_valid(x, new_x);
48+
const double new_energy = objFunc.value(new_x);
5949

60-
m_logger.trace("ls it: {} delta: {} invalid: {} ", this->cur_iter, (cur_energy - old_energy), !is_step_valid);
61-
62-
if (!std::isfinite(cur_energy) || cur_energy >= old_energy || !is_step_valid)
50+
if (!std::isfinite(new_energy))
6351
{
64-
step_size *= step_ratio;
65-
// max_step_size should return a collision free step
66-
// assert(objFunc.is_step_collision_free(x, new_x));
52+
continue;
6753
}
68-
else
54+
55+
m_logger.trace("ls it: {} ΔE: {}", cur_iter, new_energy - old_energy);
56+
57+
if (criteria(delta_x, objFunc, use_grad_norm, old_energy, old_grad, new_x, new_energy, step_size))
6958
{
70-
break;
59+
break; // found a good step size
7160
}
72-
this->cur_iter++;
7361
}
7462

7563
return step_size;
7664
}
65+
66+
bool Backtracking::criteria(
67+
const TVector &delta_x,
68+
Problem &objFunc,
69+
const bool use_grad_norm,
70+
const double old_energy,
71+
const TVector &old_grad,
72+
const TVector &new_x,
73+
const double new_energy,
74+
const double step_size) const
75+
{
76+
if (use_grad_norm)
77+
{
78+
TVector new_grad;
79+
objFunc.gradient(new_x, new_grad);
80+
return new_grad.norm() < old_grad.norm(); // TODO: cache old_grad.norm()
81+
}
82+
return new_energy < old_energy;
83+
}
84+
7785
} // namespace polysolve::nonlinear::line_search

src/polysolve/nonlinear/line_search/Backtracking.hpp

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,28 @@ namespace polysolve::nonlinear::line_search
1515

1616
virtual std::string name() override { return "Backtracking"; }
1717

18-
public:
1918
double compute_descent_step_size(
2019
const TVector &x,
2120
const TVector &delta_x,
2221
Problem &objFunc,
2322
const bool use_grad_norm,
24-
const double old_energy_in,
23+
const double old_energy,
24+
const TVector &old_grad,
2525
const double starting_step_size) override;
26+
27+
protected:
28+
virtual void init_compute_descent_step_size(
29+
const TVector &delta_x,
30+
const TVector &old_grad) {}
31+
32+
virtual bool criteria(
33+
const TVector &delta_x,
34+
Problem &objFunc,
35+
const bool use_grad_norm,
36+
const double old_energy,
37+
const TVector &old_grad,
38+
const TVector &new_x,
39+
const double new_energy,
40+
const double step_size) const;
2641
};
2742
} // namespace polysolve::nonlinear::line_search

src/polysolve/nonlinear/line_search/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ set(SOURCES
1111
MoreThuente.hpp
1212
NoLineSearch.cpp
1313
NoLineSearch.hpp
14+
RobustArmijo.cpp
15+
RobustArmijo.hpp
1416
)
1517

1618
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" PREFIX "Source Files" FILES ${SOURCES})

src/polysolve/nonlinear/line_search/CppOptArmijo.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ namespace polysolve::nonlinear::line_search
1515
Problem &objFunc,
1616
const bool use_grad_norm,
1717
const double old_energy,
18+
const TVector &old_grad,
1819
const double starting_step_size)
1920
{
2021
double step_size = cppoptlib::Armijo<Problem, 1>::linesearch(x, delta_x, objFunc, starting_step_size);
2122
// this ensures no collisions and decrease in energy
22-
return after_check.compute_descent_step_size(x, delta_x, objFunc, use_grad_norm, old_energy, step_size);
23+
return after_check.compute_descent_step_size(x, delta_x, objFunc, use_grad_norm, old_energy, old_grad, step_size);
2324
}
2425

2526
} // namespace polysolve::nonlinear::line_search

0 commit comments

Comments
 (0)