Skip to content

Commit a81933d

Browse files
authored
Fides 0.8.0 (#60)
* remove init_with_hess, add hess0 * fixup test
1 parent d085978 commit a81933d

6 files changed

Lines changed: 182 additions & 40 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
.idea/*
33
venv/*
4+
.venv/*
45
*.pyc
56
fides.egg-info/*
67
.DS_Store

fides/hessian_approximation.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,26 @@ class HessianApproximation:
1717
Abstract class from which Hessian update strategies should subclass
1818
"""
1919

20-
def __init__(self, init_with_hess: bool | None = False):
20+
def __init__(self):
2121
"""
2222
Create a Hessian update strategy instance
23-
24-
:param init_with_hess:
25-
Whether the hybrid update strategy should be initialized
26-
according to the user-provided objective function
2723
"""
2824
self._hess: np.ndarray = np.empty(0)
2925
self._diff: np.ndarray = np.empty(0)
30-
self.init_with_hess = init_with_hess
3126

32-
def init_mat(self, dim: int, hess: np.ndarray | None = None) -> None:
27+
def _init_mat(self, dim: int, hess: np.ndarray | None = None) -> None:
3328
"""
34-
Initializes this approximation instance and checks the dimensionality
29+
Initializes this approximation instance and checks the dimensionality.
30+
Note that this method is not intended to be called directly by the
31+
user.
3532
3633
:param dim:
3734
dimension of optimization variables
3835
3936
:param hess:
4037
user provided initialization
4138
"""
42-
if hess is None or not self.init_with_hess:
39+
if hess is None:
4340
self._hess = np.eye(dim)
4441
else:
4542
if hess.shape[0] != dim:
@@ -146,7 +143,6 @@ class Broyden(IterativeHessianApproximation):
146143
def __init__(
147144
self,
148145
phi: float,
149-
init_with_hess: bool | None = False,
150146
enforce_curv_cond: bool | None = True,
151147
):
152148
self.phi = phi
@@ -158,7 +154,7 @@ def __init__(
158154
'preserved during updating.',
159155
stacklevel=2,
160156
)
161-
super().__init__(init_with_hess)
157+
super().__init__()
162158

163159
def _compute_update(self, s: np.ndarray, y: np.ndarray):
164160
self._diff = broyden_class_update(
@@ -176,12 +172,10 @@ class BFGS(Broyden):
176172

177173
def __init__(
178174
self,
179-
init_with_hess: bool | None = False,
180175
enforce_curv_cond: bool | None = True,
181176
):
182177
super().__init__(
183178
phi=0.0,
184-
init_with_hess=init_with_hess,
185179
enforce_curv_cond=enforce_curv_cond,
186180
)
187181

@@ -196,12 +190,10 @@ class DFP(Broyden):
196190

197191
def __init__(
198192
self,
199-
init_with_hess: bool | None = False,
200193
enforce_curv_cond: bool | None = True,
201194
):
202195
super().__init__(
203196
phi=1.0,
204-
init_with_hess=init_with_hess,
205197
enforce_curv_cond=enforce_curv_cond,
206198
)
207199

@@ -273,9 +265,9 @@ def __init__(self, happ: IterativeHessianApproximation | None = None):
273265
self.hessian_update = happ if happ is not None else BFGS()
274266
super().__init__()
275267

276-
def init_mat(self, dim: int, hess: np.ndarray | None = None):
277-
self.hessian_update.init_mat(dim, hess)
278-
super().init_mat(dim, hess)
268+
def _init_mat(self, dim: int, hess: np.ndarray | None = None):
269+
self.hessian_update._init_mat(dim, hess)
270+
super()._init_mat(dim, hess)
279271

280272
def requires_hess(self):
281273
return True # pragma: no cover
@@ -460,12 +452,12 @@ def __init__(
460452
'preserved during updating.',
461453
stacklevel=2,
462454
)
463-
super().__init__(init_with_hess=True)
455+
super().__init__()
464456

465-
def init_mat(self, dim: int, hess: np.ndarray | None = None):
457+
def _init_mat(self, dim: int, hess: np.ndarray | None = None):
466458
self.A = np.eye(dim) * np.spacing(1)
467459
self._structured_diff = np.zeros_like(self.A)
468-
super().init_mat(dim, hess)
460+
super()._init_mat(dim, hess)
469461

470462
def update(
471463
self,

fides/minimize.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,6 @@ def __init__(
283283
self.grad_min = self.grad
284284

285285
self.hessian_update: HessianApproximation | None = hessian_update
286-
if not self.hessian_update.get_mat().empty():
287-
self.hess = self.hessian_update.get_mat()
288286
self.iterations_since_tr_update: int = 0
289287
self.n_intermediate_tr_radius: int = 0
290288

@@ -312,7 +310,12 @@ def _reset(self, start_id: str | None = None):
312310
self.start_id = start_id
313311
self.history = defaultdict(list)
314312

315-
def minimize(self, x0: np.ndarray, start_id: str | None = None):
313+
def minimize(
314+
self,
315+
x0: np.ndarray,
316+
start_id: str | None = None,
317+
hess0: np.ndarray | str | None = None,
318+
) -> tuple[float, np.ndarray, np.ndarray, np.ndarray]:
316319
"""
317320
Minimize the objective function using the interior trust-region
318321
reflective algorithm described by [ColemanLi1994] and [ColemanLi1996]
@@ -329,13 +332,25 @@ def minimize(self, x0: np.ndarray, start_id: str | None = None):
329332
options[`maxtime`] on the next iteration.
330333
331334
:param x0:
332-
initial guess
335+
initial guess for the optimization variables
336+
337+
:param start_id:
338+
optional identifier for this optimization run, used for history
339+
tracking
340+
341+
:param hess0:
342+
optional initial Hessian approximation. If a string 'hess' is
343+
provided, the initial Hessian from the objective function
344+
evaluation at x0 is used. Otherwise, a numpy array of shape
345+
(n,n) must be provided, where n is the number of optimization
346+
variables.
333347
334348
:returns:
335349
fval: final function value,
336350
x: final optimization variable values,
337351
grad: final gradient,
338352
hess: final Hessian (approximation)
353+
339354
"""
340355
self._reset(start_id)
341356

@@ -349,8 +364,11 @@ def minimize(self, x0: np.ndarray, start_id: str | None = None):
349364

350365
self.fval, self.grad = funout.fval, funout.grad
351366
if self.hessian_update is not None:
352-
if self.hessian_update.get_mat().empty():
353-
self.hessian_update.init_mat(len(self.x), funout.hess)
367+
if isinstance(hess0, str) and hess0 == 'hess':
368+
self.hessian_update._init_mat(len(self.x), funout.hess)
369+
else:
370+
self.hessian_update._init_mat(len(self.x), hess0)
371+
self.hess = self.hessian_update.get_mat().copy()
354372
else:
355373
self.hess = funout.hess.copy()
356374

fides/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.7.9'
1+
__version__ = '0.8.0'

tests/test_hessian_approximation.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55

66

77
def test_wrong_dim():
8-
h = BFGS(init_with_hess=True)
8+
h = BFGS()
99
with pytest.raises(ValueError):
10-
h.init_mat(dim=3, hess=np.ones((2, 2)))
10+
h._init_mat(dim=3, hess=np.ones((2, 2)))
1111

1212
h = BFGS()
13-
h.init_mat(dim=3)
13+
h._init_mat(dim=3)
1414
with pytest.raises(ValueError):
1515
h.set_mat(np.ones((2, 2)))
1616

1717

1818
def test_broyden():
1919
h = Broyden(phi=2)
20-
h.init_mat(dim=2)
20+
h._init_mat(dim=2)
2121
h.update(np.random.random((2, 1)), np.random.random((2, 1)))
2222

2323
h = Broyden(phi=-1)
24-
h.init_mat(dim=2)
24+
h._init_mat(dim=2)
2525
h.update(np.random.random((2,)), np.random.random((2,)))

tests/test_minimize.py

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,11 @@ def unbounded_and_init():
185185
(rosengrad, BB()), # 5
186186
(rosengrad, Broyden(0.5)), # 6
187187
(rosenboth, HybridFixed(BFGS())), # 7
188-
(rosenboth, HybridFixed(SR1())), # 8
189-
(rosenboth, HybridFixed(BFGS(init_with_hess=True))), # 9
190-
(rosenboth, HybridFixed(SR1(init_with_hess=True))), # 10
188+
(rosenboth, HybridFixed(SR1())), # 8 # 10
191189
(rosenboth, HybridFraction(BFGS())), # 11
192190
(rosenboth, HybridFraction(SR1())), # 12
193-
(rosenboth, HybridFraction(BFGS(init_with_hess=True))), # 13
194-
(rosenboth, HybridFraction(SR1(init_with_hess=True))), # 14
195191
(fletcher, FX(BFGS())), # 15
196192
(fletcher, FX(SR1())), # 16
197-
(fletcher, FX(BFGS(init_with_hess=True))), # 17
198-
(fletcher, FX(SR1(init_with_hess=True))), # 18
199193
(fletcher, SSM(0.0)), # 19
200194
(fletcher, SSM(0.5)), # 20
201195
(fletcher, SSM(1.0)), # 21
@@ -494,3 +488,140 @@ def test_wrong_options():
494488
verbose=logging.INFO,
495489
options={Options.SUBSPACE_DIM: '2D'},
496490
)
491+
492+
493+
def test_hess0_initialization():
494+
"""
495+
Test that hess0 parameter correctly initializes Hessian approximation.
496+
"""
497+
lb, ub, x0 = finite_bounds_include_optimum()
498+
fun = rosengrad
499+
fun_with_hess = rosenboth
500+
501+
# Test 1: Verify hess0 is used when provided with hessian_update
502+
custom_hess0 = np.eye(len(x0)) * 10.0
503+
opt_with_hess0 = Optimizer(
504+
fun,
505+
ub=ub,
506+
lb=lb,
507+
verbose=logging.WARNING,
508+
options={Options.MAXITER: 1}, # Only run one iteration
509+
hessian_update=BFGS(),
510+
)
511+
opt_with_hess0.minimize(x0, hess0=custom_hess0)
512+
assert opt_with_hess0.hess is not None
513+
514+
# Test 2: Verify default initialization when hess0 is not provided
515+
opt_without_hess0 = Optimizer(
516+
fun,
517+
ub=ub,
518+
lb=lb,
519+
verbose=logging.WARNING,
520+
options={Options.MAXITER: 1},
521+
hessian_update=BFGS(),
522+
)
523+
opt_without_hess0.minimize(x0)
524+
525+
# Test 3: Verify hess0 has correct dimensions
526+
wrong_dim_hess0 = np.eye(len(x0) + 1)
527+
opt_wrong_dim = Optimizer(
528+
fun,
529+
ub=ub,
530+
lb=lb,
531+
verbose=logging.WARNING,
532+
hessian_update=BFGS(),
533+
)
534+
with pytest.raises(ValueError):
535+
opt_wrong_dim.minimize(x0, hess0=wrong_dim_hess0)
536+
537+
# Test 4: Verify hess0 works with different update schemes
538+
for happ_class in [BFGS, DFP, SR1, Broyden]:
539+
happ = happ_class() if happ_class != Broyden else Broyden(phi=0.5)
540+
custom_hess = np.eye(len(x0)) * 5.0
541+
opt = Optimizer(
542+
fun,
543+
ub=ub,
544+
lb=lb,
545+
verbose=logging.WARNING,
546+
options={Options.MAXITER: 2, Options.FATOL: 0},
547+
hessian_update=happ,
548+
)
549+
opt.minimize(x0, hess0=custom_hess)
550+
assert opt.iteration >= 1, f'Failed for {happ_class.__name__}'
551+
552+
# Test 5: Verify hess0 is ignored when no hessian_update is provided
553+
opt_no_update = Optimizer(
554+
fun_with_hess,
555+
ub=ub,
556+
lb=lb,
557+
verbose=logging.WARNING,
558+
options={Options.MAXITER: 1},
559+
)
560+
hess0_ignored = np.eye(len(x0)) * 100.0
561+
opt_no_update.minimize(x0, hess0=hess0_ignored)
562+
563+
# Test 6: Test initialization with exact Hessian
564+
opt_hess_init = Optimizer(
565+
fun_with_hess,
566+
ub=ub,
567+
lb=lb,
568+
verbose=logging.WARNING,
569+
options={Options.MAXITER: 10, Options.FATOL: 1e-8},
570+
hessian_update=HybridFixed(BFGS()),
571+
)
572+
opt_hess_init.minimize(x0, hess0='hess')
573+
iterations_with_hess = opt_hess_init.iteration
574+
575+
# Compare with BFGS without using initial Hessian
576+
opt_no_hess_init = Optimizer(
577+
fun,
578+
ub=ub,
579+
lb=lb,
580+
verbose=logging.WARNING,
581+
options={Options.MAXITER: 10, Options.FATOL: 1e-8},
582+
hessian_update=BFGS(),
583+
)
584+
opt_no_hess_init.minimize(x0)
585+
iterations_without_hess = opt_no_hess_init.iteration
586+
587+
# Using exact Hessian for initialization should help convergence
588+
assert iterations_with_hess <= iterations_without_hess or (
589+
opt_hess_init.converged and opt_no_hess_init.converged
590+
), 'Hessian initialization should help convergence'
591+
592+
# Test 8: Verify hess0 affects convergence behavior
593+
true_hess_at_x0 = np.array(
594+
[
595+
[1200 * x0[0] ** 2 - 400 * x0[1] + 2, -400 * x0[0]],
596+
[-400 * x0[0], 200],
597+
]
598+
)
599+
600+
opt_good_init = Optimizer(
601+
fun,
602+
ub=ub,
603+
lb=lb,
604+
verbose=logging.WARNING,
605+
options={Options.MAXITER: 100, Options.FATOL: 1e-8},
606+
hessian_update=BFGS(),
607+
)
608+
opt_good_init.minimize(x0, hess0=true_hess_at_x0)
609+
iterations_good = opt_good_init.iteration
610+
611+
# Use a poor initial Hessian approximation
612+
poor_hess = np.eye(len(x0)) * 0.01
613+
opt_poor_init = Optimizer(
614+
fun,
615+
ub=ub,
616+
lb=lb,
617+
verbose=logging.WARNING,
618+
options={Options.MAXITER: 100, Options.FATOL: 1e-8},
619+
hessian_update=BFGS(),
620+
)
621+
opt_poor_init.minimize(x0, hess0=poor_hess)
622+
iterations_poor = opt_poor_init.iteration
623+
624+
# Good initialization should converge in fewer or equal iterations
625+
assert iterations_good <= iterations_poor or (
626+
opt_good_init.converged and opt_poor_init.converged
627+
), 'Good Hessian initialization should help convergence'

0 commit comments

Comments
 (0)