diff --git a/fides/minimize.py b/fides/minimize.py index cd52b4b..29f3c88 100644 --- a/fides/minimize.py +++ b/fides/minimize.py @@ -712,7 +712,15 @@ def update_tr_radius( else: self.tr_ratio = actual_decrease / predicted_decrease - interior_solution = nsx < self.delta_iter * 0.9 + # the radius is only increased if the trust region constraint + # was active. For steps that were stepped back to stay within + # the bounds (step.alpha < 1), this is decided based on the + # proposal before step back. The stepped back step is short + # because of the bounds, not the trust region, e.g., a single + # reflection that is cut short by the next bound. + interior_solution = ( + norm(step.og_ss + step.ss0) < self.delta_iter * 0.9 + ) if ( self.tr_ratio >= self.get_option(Options.ETA) diff --git a/tests/test_minimize.py b/tests/test_minimize.py index 5868412..683e011 100644 --- a/tests/test_minimize.py +++ b/tests/test_minimize.py @@ -1537,3 +1537,38 @@ def test_reflected_step_roundoff_stays_in_bounds(subspace_dim): assert np.all(x <= ub) assert opt.exitflag > 0 assert np.isclose(fval, 10.913558700882975, rtol=1e-6) + + +@pytest.mark.parametrize('stepback', list(StepBackStrategy)) +@pytest.mark.parametrize('subspace_dim', [SubSpaceDim.TWO, SubSpaceDim.FULL]) +def test_several_active_bounds(subspace_dim, stepback): + """ + gh-78: with reflect_single, the reflected segment is cut short by the + second active bound. The step is then shorter than the radius, the + radius was never increased and the run crawled to MAXITER. + """ + hess = np.diag([2.0, 1.0, 1.5, 1.2]) + c = np.array([-1000.0, 1000.0, -1000.0, 1000.0]) + + def fun(x): + d = x - c + g = hess @ d + return 0.5 * d @ g, g, hess + + opt = Optimizer( + fun, + ub=np.full(4, np.inf), + lb=np.array([-1.0, -np.inf, -1.0, -np.inf]), + verbose=logging.WARNING, + options={ + Options.MAXITER: 100, + Options.SUBSPACE_DIM: subspace_dim, + Options.STEPBACK_STRAT: stepback, + }, + ) + fval, x, _, _ = opt.minimize(np.array([-1.0, 0.0, -1.0, 0.0])) + # full/refine stops with DELTA_TOO_SMALL at the optimum, as + # the predicted decrease there is below the resolution of fval + assert opt.exitflag != fides.ExitFlag.MAXITER + assert np.isclose(fval, 1746501.75, rtol=1e-8) + assert np.allclose(x, [-1.0, 1000.0, -1.0, 1000.0], rtol=1e-5)