Skip to content

Commit 81989ea

Browse files
committed
Add relative cost decrease and gradient norm exit criteria to batch solver.
1 parent 9611b74 commit 81989ea

File tree

4 files changed

+120
-10
lines changed

4 files changed

+120
-10
lines changed

examples/ex_batch_se3.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
def main():
1313
# ##########################################################################
1414
# Create the batch estimator with desired settings
15-
estimator = nav.BatchEstimator(solver_type="GN", max_iters=20)
15+
estimator = nav.BatchEstimator(solver_type="GN", max_iters=30, step_tol=1e-7, gradient_tol=1e-7, ftol=1e-8, verbose=True)
1616

1717
# ##########################################################################
1818
# Problem Setup

examples/ex_batch_vector.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
def main():
1717
# #############################################################################
1818
# Create the batch estimator with desired settings
19-
estimator = nav.BatchEstimator(solver_type="GN", max_iters=5)
19+
estimator = nav.BatchEstimator(solver_type="LM", max_iters=20, step_tol=None, gradient_tol=1e-7, ftol=1e-8, verbose=True)
2020

2121
# ##############################################################################
2222
# Problem Setup

navlie/batch/estimator.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def __init__(
3535
solver_type: str = "GN",
3636
max_iters: int = 100,
3737
step_tol: float = 1e-7,
38+
ftol: float = None,
39+
gradient_tol: float = None,
3840
tau: float = 1e-11,
3941
verbose: bool = True,
4042
):
@@ -47,7 +49,28 @@ def __init__(
4749
max_iters : int, optional
4850
Maximum number of optimization iterations, by default 100.
4951
step_tol : float, optional
50-
Convergence tolerance, by default 1e7.
52+
Convergence step tolerance, by default 1e-7.
53+
The solver exits when
54+
55+
.. math::
56+
57+
||\Delta x||_2 < \\text{step_tol}
58+
where :math:`\Delta x` is the change in the state estimate for successive steps.
59+
ftol : float, optional
60+
Convergence relative cost decrease tolerance, by default None (not used).
61+
The solver exits when
62+
63+
.. math::
64+
65+
|\Delta C /C| < \\text{ftol}
66+
where :math:`\Delta C` is change in the cost function for successive accepted steps.
67+
gradient_tol : float, optional
68+
Convergence gradient infinity norm tolerance, by default None (not used).
69+
The solver exits when
70+
71+
.. math::
72+
73+
\max_i |\\nabla J|_i = \max_i |\mathbf{e}^T \mathbf{H}|_i < \\text{gradient_tol}
5174
tau : float, optional
5275
tau parameter in LM, by default 1e-11.
5376
verbose : bool, optional
@@ -56,6 +79,8 @@ def __init__(
5679
self.solver_type = solver_type
5780
self.max_iters = max_iters
5881
self.step_tol = step_tol
82+
self.ftol = ftol
83+
self.gradient_tol = gradient_tol
5984
self.tau = tau
6085
self.verbose = verbose
6186

@@ -154,6 +179,8 @@ def solve(
154179
max_iters=self.max_iters,
155180
solver=self.solver_type,
156181
step_tol=self.step_tol,
182+
ftol=self.ftol,
183+
gradient_tol=self.gradient_tol,
157184
tau=self.tau,
158185
verbose=self.verbose,
159186
)

navlie/batch/problem.py

+90-7
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,17 @@ def __init__(
5858
solver: str = "GN",
5959
max_iters: int = 100,
6060
step_tol: float = 1e-7,
61+
ftol: float = None,
62+
gradient_tol: float = None,
6163
tau: float = 1e-11,
6264
verbose: bool = True,
6365
):
6466
# Set solver parameters
6567
self.solver = solver
6668
self.max_iters = max_iters
6769
self.step_tol = step_tol
70+
self.ftol = ftol
71+
self.gradient_tol = gradient_tol
6872
self.tau = tau
6973
self.verbose = verbose
7074

@@ -91,6 +95,27 @@ def __init__(
9195
# Inverse of information matrix
9296
self._covariance_matrix: np.ndarray = None
9397

98+
def is_converged(self, delta_cost, cost, dx, grad_norm) -> bool:
99+
converged = False
100+
if delta_cost is not None:
101+
rel_cost_change = 0.0
102+
if cost != 0:
103+
rel_cost_change = delta_cost / cost
104+
105+
if self.step_tol is not None and dx < self.step_tol:
106+
converged = True
107+
if self.ftol is not None and delta_cost is not None:
108+
if rel_cost_change < self.ftol:
109+
converged = True
110+
if cost == 0.0:
111+
converged = True
112+
if dx == 0.0:
113+
converged = True
114+
if self.gradient_tol is not None and grad_norm is not None:
115+
if grad_norm < self.gradient_tol:
116+
converged = True
117+
return converged
118+
94119
def add_residual(self, residual: Residual, loss: LossFunction = L2Loss()):
95120
"""Adds a residual to the problem, along with a robust loss
96121
function to use. Default loss function is the standard L2Loss.
@@ -182,6 +207,10 @@ def _solve_gauss_newton(self) -> Dict[Hashable, State]:
182207
"""
183208

184209
dx = 10
210+
delta_cost = None
211+
rel_cost_decrease = None
212+
grad_norm = None
213+
185214
iter_idx = 0
186215
cost_list = []
187216

@@ -193,7 +222,11 @@ def _solve_gauss_newton(self) -> Dict[Hashable, State]:
193222
header = "Initial cost: " + str(cost)
194223
print(header)
195224

196-
while (iter_idx < self.max_iters) and (dx > self.step_tol):
225+
while iter_idx < self.max_iters:
226+
227+
if self.is_converged(delta_cost, cost_list[-1], dx, grad_norm):
228+
break
229+
197230
H_spr = sparse.csr_matrix(H)
198231

199232
A = H_spr.T @ H_spr
@@ -208,8 +241,23 @@ def _solve_gauss_newton(self) -> Dict[Hashable, State]:
208241
cost_list.append(cost)
209242

210243
dx = np.linalg.norm(delta_x)
244+
if len(cost_list) >= 2:
245+
delta_cost = np.abs(cost_list[-1] - cost_list[-2])
246+
if cost_list[-1] != 0:
247+
rel_cost_decrease = delta_cost / cost_list[-1]
248+
else:
249+
rel_cost_decrease = 0
250+
grad_norm = np.max(np.abs((e.T @ H).squeeze()))
251+
211252
if self.verbose:
212-
self._display_header(iter_idx, cost, dx)
253+
self._display_header(
254+
iter_idx,
255+
cost,
256+
dx,
257+
delta_cost,
258+
rel_cost_decrease,
259+
grad_norm,
260+
)
213261

214262
iter_idx += 1
215263

@@ -232,6 +280,10 @@ def _solve_LM(self) -> Dict[Hashable, State]:
232280
"""
233281

234282
e, H, cost = self.compute_error_jac_cost()
283+
284+
delta_cost = None
285+
rel_cost_decrease = None
286+
grad_norm = None
235287
cost_list = [cost]
236288

237289
H_spr = sparse.csr_matrix(H)
@@ -250,9 +302,12 @@ def _solve_LM(self) -> Dict[Hashable, State]:
250302
print(header)
251303

252304
# Main LM loop
253-
while (iter_idx < self.max_iters) and (dx > self.step_tol):
305+
while iter_idx < self.max_iters:
254306
A_solve = A + mu * sparse.identity(A.shape[0])
255307
delta_x = sparse.linalg.spsolve(A_solve, -b).reshape((-1, 1))
308+
dx = np.linalg.norm(delta_x)
309+
if self.is_converged(delta_cost, cost_list[-1], dx, grad_norm):
310+
break
256311

257312
variables_test = {k: v.copy() for k, v in self.variables.items()}
258313

@@ -287,10 +342,26 @@ def _solve_LM(self) -> Dict[Hashable, State]:
287342
nu = 2 * nu
288343
status = "Rejected."
289344

290-
dx = np.linalg.norm(delta_x)
345+
346+
347+
if len(cost_list) >= 2:
348+
delta_cost = np.abs(cost_list[-1] - cost_list[-2])
349+
if cost_list[-1] != 0:
350+
rel_cost_decrease = delta_cost / cost_list[-1]
351+
else:
352+
rel_cost_decrease = 0
353+
grad_norm = np.max(np.abs((e.T @ H).squeeze()))
291354

292355
if self.verbose:
293-
self._display_header(iter_idx + 1, cost, dx, status=status)
356+
self._display_header(
357+
iter_idx,
358+
cost,
359+
dx,
360+
delta_cost,
361+
rel_cost_decrease,
362+
grad_norm,
363+
status=status,
364+
)
294365

295366
iter_idx += 1
296367

@@ -479,7 +550,14 @@ def compute_covariance(self):
479550
return None
480551

481552
def _display_header(
482-
self, iter_idx: int, current_cost: float, dx: float, status: str = None
553+
self,
554+
iter_idx: int,
555+
current_cost: float,
556+
dx: float,
557+
delta_cost: float = None,
558+
delta_cost_rel: float = None,
559+
grad_norm: float = None,
560+
status: str = None,
483561
):
484562
"""Displays the optimization progress.
485563
@@ -497,7 +575,12 @@ def _display_header(
497575
header = ("Iter: {0} || Cost: {1:.4e} || Step size: {2:.4e}").format(
498576
iter_idx, current_cost, dx
499577
)
500-
578+
if delta_cost is not None:
579+
header += " || dC: {0:.4e}".format(delta_cost)
580+
if delta_cost_rel is not None:
581+
header += " || dC/C: {0:.4e}".format(delta_cost_rel)
582+
if grad_norm is not None:
583+
header += " || |grad|_inf: {0:.4e}".format(grad_norm)
501584
if status is not None:
502585
header += " || Status: " + status
503586

0 commit comments

Comments
 (0)