Skip to content

Commit 1e405b7

Browse files
authored
Merge pull request #30 from fides-dev/develop
Fides 0.4.0
2 parents ae15ab6 + 17edaca commit 1e405b7

12 files changed

Lines changed: 203 additions & 84 deletions

LICENSE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# License conditions
22

3-
## pytr
3+
## fides
44

5-
pytr is released under the 3-Clause BSD License (BSD-3-Clause) with the
5+
fides is released under the 3-Clause BSD License (BSD-3-Clause) with the
66
following terms:
77

8-
Copyright (c) 2020, Fabian Fröhlich
8+
Copyright (c) 2020-2021, Fabian Fröhlich
99
All rights reserved.
1010

1111
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@
1111

1212
## About Fides
1313

14-
Fides implements an Interior Trust Region Reflective for boundary costrained
15-
optimization problems based on the papers [ColemanLi1994] and [ColemanLi1996
16-
]. Accordingly, Fides is named after the Roman goddess of trust and
14+
Fides implements an Interior Trust Region Reflective for boundary constrained
15+
optimization problems based on the papers
16+
[ColemanLi1994](https://doi.org/10.1007/BF01582221) and
17+
[ColemanLi1996](http://dx.doi.org/10.1137/0806023). Accordingly, Fides is named
18+
after the Roman goddess of trust and
1719
reliability. In contrast to other optimizers, Fides solves the full trust
18-
-region subproblem exactly, which can yields higher quality proposal steps, but
20+
-region subproblem exactly, which can yield higher quality proposal steps, but
1921
is computationally more expensive. This makes Fides particularly attractive
2022
for optimization problems with objective functions that are computationally
2123
expensive to evaluate and the computational cost of solving the trust
2224
-region subproblem is negligible.
2325

2426
Fides can be installed via `pip install fides`. Further documentation is
25-
avaliable at [Read the Docs](fides-optimizer.readthedocs.io).
27+
available at [Read the Docs](https://fides-optimizer.readthedocs.io/).
2628

2729

2830
## Features

fides/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class SubSpaceDim(str, enum.Enum):
3939
"""
4040
TWO = '2D' #: Two dimensional Newton/Gradient subspace
4141
FULL = 'full' #: Full :math:`\mathbb{R}^n`
42+
STEIHAUG = 'scg' #: CG subspace via Steihaug's method
4243

4344

4445
class StepBackStrategy(str, enum.Enum):

fides/hessian_approximation.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
-------------------------
44
This module provides various generic Hessian approximation strategies that
55
can be employed when the calculating the exact Hessian or an approximation
6-
is computationally too demandind.
6+
is computationally too demanding.
77
"""
88

99

@@ -21,7 +21,8 @@ def __init__(self, hess_init: Optional[np.ndarray] = None):
2121
Create a Hessian update strategy instance
2222
2323
:param hess_init:
24-
Inital guess for the Hessian, if empty Identity matrix will be used
24+
Initial guess for the Hessian, if empty Identity matrix will be
25+
used
2526
"""
2627
self.hess_init = None
2728
if hess_init is not None:
@@ -33,7 +34,8 @@ def set_init(self, hess_init: np.ndarray):
3334
Create a Hessian update strategy instance
3435
3536
:param hess_init:
36-
Inital guess for the Hessian, if empty Identity matrix will be used
37+
Initial guess for the Hessian, if empty Identity matrix will be
38+
used
3739
"""
3840
if not isinstance(hess_init, np.ndarray):
3941
raise ValueError('Cannot initialize with hess_init of type'
@@ -62,7 +64,7 @@ def init_mat(self, dim: int):
6264
else:
6365
self._hess = self.hess_init.copy()
6466
if self._hess.shape[0] != dim:
65-
raise ValueError('Inital approximation had inconsistent '
67+
raise ValueError('Initial approximation had inconsistent '
6668
f'dimension, was {self._hess.shape[0]}, '
6769
f'but should be {dim}.')
6870

fides/minimize.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,16 @@ def _reset(self):
128128

129129
def minimize(self, x0: np.ndarray):
130130
"""
131-
Minimize the objective function the interior trust-region reflective
132-
algorithm described by [ColemanLi1994] and [ColemanLi1996]
131+
Minimize the objective function using the interior trust-region
132+
reflective algorithm described by [ColemanLi1994] and [ColemanLi1996]
133133
Convergence with respect to function value is achieved when
134134
math:`|f_{k+1} - f_k|` < options[`fatol`] - :math:`f_k` options[
135-
`frtol`]. Similarly, convergence with respect to optimization
135+
`frtol`]. Similarly, convergence with respect to optimization
136136
variables is achieved when :math:`||x_{k+1} - x_k||` < options[
137-
`xatol`] - :math:`x_k` options[`xrtol`]. Convergence with respect
138-
to the gradient is achieved when :math:`||g_k||` <
139-
options[`gatol`] or `||g_k||` < options[`grtol`] * `f_k`. Other than
137+
`xtol`] :math:`x_k` (note that this is checked in transformed
138+
coordinates that account for distance to boundaries). Convergence
139+
with respect to the gradient is achieved when :math:`||g_k||` <
140+
options[`gatol`] or `||g_k||` < options[`grtol`] * `f_k`. Other than
140141
that, optimization can be terminated when iterations exceed
141142
options[ `maxiter`] or the elapsed time is expected to exceed
142143
options[`maxtime`] on the next iteration.
@@ -194,12 +195,6 @@ def minimize(self, x0: np.ndarray):
194195
f'x has {len(self.x)} entries but gradient has '
195196
f'{len(self.grad)}!')
196197

197-
if not len(self.grad) == len(self.x):
198-
raise ValueError('Provided objective function must return a '
199-
'gradient vector of the same shape as x, '
200-
f'x has {len(self.x)} entries but gradient has '
201-
f'{len(self.grad)}!')
202-
203198
# hessian approximation would error on these earlier
204199
if not self.hess.ndim == 2:
205200
raise ValueError('Provided objective function must return a '
@@ -212,7 +207,7 @@ def minimize(self, x0: np.ndarray):
212207

213208
if not self.hess.shape[0] == len(self.x):
214209
raise ValueError('Provided objective function must return a '
215-
'square Hessian matrix with same dimension'
210+
'square Hessian matrix with same dimension as x. '
216211
f'x has {len(self.x)} entries but Hessian has '
217212
f'{self.hess.shape[0]}!')
218213

@@ -365,16 +360,17 @@ def update_tr_radius(self,
365360

366361
# values as proposed in algorithm 4.1 in Nocedal & Wright
367362
if self.tr_ratio >= self.get_option(Options.ETA) \
368-
and not interior_solution:
363+
and not interior_solution and step.qpval <= 0:
369364
# increase radius
370365
self.delta = self.get_option(Options.GAMMA2) * self.delta
371-
elif self.tr_ratio <= self.get_option(Options.MU):
366+
elif self.tr_ratio <= self.get_option(Options.MU) or \
367+
step.qpval > 0:
372368
# decrease radius
373369
self.delta = np.nanmin([
374370
self.delta * self.get_option(Options.GAMMA1),
375371
nsx / 4
376372
])
377-
return self.tr_ratio > 0.0
373+
return self.tr_ratio > 0.0 and step.qpval <= 0
378374

379375
def check_convergence(self, step: Step, fval: float,
380376
grad: np.ndarray) -> None:
@@ -475,8 +471,7 @@ def check_continue(self) -> bool:
475471
self.exitflag = ExitFlag.DELTA_TOO_SMALL
476472
self.logger.warning(
477473
f'Stopping as trust region radius {self.delta:.2E} is '
478-
f'smaller '
479-
'than machine precision.'
474+
f'smaller than machine precision.'
480475
)
481476
return False
482477

@@ -498,7 +493,7 @@ def make_non_degenerate(self, eps=1e2 * np.spacing(1)) -> None:
498493

499494
def get_affine_scaling(self) -> Tuple[np.ndarray, np.ndarray]:
500495
"""
501-
Computes the vector v and dv, the diagonal of it's Jacobian. For the
496+
Computes the vector v and dv, the diagonal of its Jacobian. For the
502497
definition of v, see Definition 2 in [Coleman-Li1994]
503498
504499
:return:

fides/stepback.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def stepback_reflect(tr_step: Step,
2727
Compute new proposal steps according to a reflection strategy.
2828
2929
:param tr_step:
30-
Reference trust region step that will be reflect
30+
Reference trust region step that will be reflected
3131
:param x:
3232
Current values of the optimization variables
3333
:param sg:

fides/steps.py

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,21 @@
88

99
import numpy as np
1010
import scipy.linalg as linalg
11+
import scipy.sparse.linalg as slinalg
1112

1213
from numpy.linalg import norm
1314
from scipy.sparse import csc_matrix
1415
from scipy.optimize import Bounds, NonlinearConstraint, minimize
1516

1617
from logging import Logger
1718
from .subproblem import (
18-
solve_1d_trust_region_subproblem, solve_nd_trust_region_subproblem
19+
solve_1d_trust_region_subproblem, solve_nd_trust_region_subproblem,
20+
get_1d_trust_region_boundary_solution, quadratic_form
1921
)
2022

2123
from typing import Union
2224

2325

24-
def quadratic_form(Q: np.ndarray, p: np.ndarray, x: np.ndarray) -> float:
25-
"""
26-
Computes the quadratic form :math:`x^TQx + x^Tp`
27-
28-
:param Q: Matrix
29-
:param p: Vector
30-
:param x: Input
31-
32-
:return:
33-
Value of form
34-
"""
35-
return 0.5 * x.T.dot(Q).dot(x) + p.T.dot(x)
36-
37-
3826
def normalize(v: np.ndarray) -> None:
3927
"""
4028
Inplace normalization of a vector
@@ -57,7 +45,7 @@ class Step:
5745
transformed step ss: `ss = subspace * sc`
5846
:ivar ss: Affine transformed step: `s = scaling * ss`
5947
:ivar og_s: `s` without step back
60-
:ivar og_sc: `st` without step back
48+
:ivar og_sc: `sc` without step back
6149
:ivar og_ss: `ss` without step back
6250
:ivar sg: Rescaled gradient `scaling * g`
6351
:ivar hess: Hessian of the objective function at `x`
@@ -238,7 +226,7 @@ class TRStepFull(Step):
238226
the trust region subproblem.
239227
"""
240228

241-
type = 'trnd'
229+
type = 'nd'
242230

243231
def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
244232
ub, lb, logger):
@@ -253,7 +241,7 @@ class TRStep2D(Step):
253241
the trust region subproblem according to a 2D subproblem
254242
"""
255243

256-
type = 'tr2d'
244+
type = '2d'
257245

258246
def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
259247
ub, lb, logger):
@@ -270,8 +258,11 @@ def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
270258
# in this case we are in Case 2 of Fig 12 in
271259
# [Coleman-Li1994]
272260
logger.debug('Newton direction did not have negative '
273-
'curvature adding scaling * np.sign(sg) to '
274-
'2D subspace.')
261+
'using scaling * np.sign(sg) and ev to smallest '
262+
'eigenvalue instead.')
263+
e, v = slinalg.eigs(self.shess, k=1, which='SR')
264+
s_newt = v[:, np.argmin(e)]
265+
normalize(s_newt)
275266
s_grad = scaling * np.sign(sg) + (sg == 0)
276267
else:
277268
s_grad = sg.copy()
@@ -290,6 +281,78 @@ def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
290281
self.subspace = np.expand_dims(s_newt, 1)
291282

292283

284+
class CGStep(Step):
285+
"""
286+
This class provides the machinery to compute an approximate solution of
287+
the trust region subproblem using the Steihaug Method
288+
"""
289+
290+
type = 'cg'
291+
292+
def calculate(self):
293+
nsg = norm(self.sg)
294+
self.conj_grad(min(0.5, np.sqrt(nsg)) * nsg)
295+
self.s = self.scaling.dot(self.ss + self.ss0)
296+
self.step_back()
297+
self.qpval = quadratic_form(self.shess, self.sg, self.ss + self.ss0)
298+
299+
def conj_grad(self, eps):
300+
"""
301+
Compute step proposal using conjugate gradient method
302+
303+
:param eps:
304+
tolerance for residual norm
305+
"""
306+
raise NotImplementedError()
307+
308+
309+
class TRStepSteihaug(CGStep):
310+
"""
311+
This class provides the machinery to compute an approximate solution of
312+
the trust region subproblem using the Steihaug Method
313+
"""
314+
315+
type = 'cgs'
316+
317+
def conj_grad(self, eps):
318+
z = np.zeros_like(self.sg)
319+
r = self.sg.copy()
320+
d = -self.sg.copy()
321+
if norm(r) < eps:
322+
self.ss = z
323+
return
324+
325+
while True:
326+
bd = self.shess.dot(d)
327+
c = d.dot(bd)
328+
r2 = np.dot(r, r)
329+
alpha = r2 / c
330+
zp = z + alpha * d
331+
if c <= 0 or norm(zp) >= self.delta:
332+
self.subspace = np.expand_dims(d, 1)
333+
self.ss0 = z
334+
self.sc = get_1d_trust_region_boundary_solution(
335+
self.shess, self.sg, self.subspace[:, 0], self.ss0,
336+
self.delta
337+
) * np.ones((1,))
338+
self.ss = self.subspace.dot(self.sc)
339+
return
340+
rp = r + alpha*bd
341+
rp2 = np.dot(rp, rp)
342+
if np.sqrt(rp2) < eps:
343+
normalize(d)
344+
self.subspace = np.expand_dims(d, 1)
345+
self.sc = zp.dot(d) * np.ones((1,))
346+
self.ss = self.subspace.dot(self.sc)
347+
self.ss0 = zp - self.ss
348+
return
349+
beta = rp2 / r2
350+
351+
d = -rp + beta * d
352+
z = zp
353+
r = rp
354+
355+
293356
class TRStepReflected(Step):
294357
"""
295358
This class provides the machinery to compute a reflected step based on
@@ -374,6 +437,22 @@ def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
374437
self.subspace = np.expand_dims(s_grad, 1)
375438

376439

440+
class ScaledGradientStep(Step):
441+
"""
442+
This class provides the machinery to compute a gradient step.
443+
"""
444+
445+
type = 'dg'
446+
447+
def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
448+
ub, lb, logger):
449+
super().__init__(x, sg, hess, scaling, g_dscaling, delta, theta,
450+
ub, lb, logger)
451+
s_grad = scaling*sg.copy()
452+
normalize(s_grad)
453+
self.subspace = np.expand_dims(s_grad, 1)
454+
455+
377456
class RefinedStep(Step):
378457
"""
379458
This class provides the machinery to refine a step based on interior
@@ -393,8 +472,8 @@ def __init__(self, x, sg, hess, scaling, g_dscaling, delta, theta,
393472
NonlinearConstraint(
394473
fun=lambda xs: (norm(xs) - delta) * np.ones((1,)),
395474
jac=lambda xs: np.expand_dims(xs, 1).T / norm(xs),
396-
lb=np.zeros((1,)),
397-
ub=np.ones((1,)) * np.inf,
475+
lb=-np.ones((1,)) * np.inf,
476+
ub=np.zeros((1,)),
398477
)
399478
]
400479
self.guess = step.ss + step.ss0

0 commit comments

Comments
 (0)