diff --git a/cyipopt/cython/ipopt_wrapper.pyx b/cyipopt/cython/ipopt_wrapper.pyx index 1497ed0..a6d0a6a 100644 --- a/cyipopt/cython/ipopt_wrapper.pyx +++ b/cyipopt/cython/ipopt_wrapper.pyx @@ -592,6 +592,15 @@ cdef class Problem: x : array-like, shape(n, ) Initial guess. + lagrange : array-like, shape(m, ), optional (default=[]) + Initial values for the constraint multipliers (only if warm start option is chosen). + + zl : array-like, shape(n, ), optional (default=[]) + Initial values for the multipliers for lower variable bounds (only if warm start option is chosen). + + zu : array-like, shape(n, ), optional (default=[]) + Initial values for the multipliers for upper variable bounds (only if warm start option is chosen). + Returns ------- x : array, shape(n, ) @@ -625,14 +634,27 @@ cdef class Problem: cdef ApplicationReturnStatus stat cdef np.ndarray[DTYPEd_t, ndim=1] g = np.zeros((self.__m,), dtype=DTYPEd) - if lagrange == []: + lagrange = np.atleast_1d(lagrange) + zl = np.atleast_1d(zl) + zu = np.atleast_1d(zu) + + if len(lagrange) == 0: lagrange = np.zeros((self.__m,), dtype=DTYPEd) + elif self.__m != len(lagrange): + raise ValueError("Wrong length of lagrange.") + cdef np.ndarray[DTYPEd_t, ndim=1] mult_g = np.array(lagrange, dtype=DTYPEd).flatten() - if zl == []: + if len(zl) == 0: zl = np.zeros((self.__n,), dtype=DTYPEd) - if zu == []: + elif self.__n != len(zl): + raise ValueError("Wrong length of zl.") + + if len(zu) == 0: zu = np.zeros((self.__n,), dtype=DTYPEd) + elif self.__n != len(zu): + raise ValueError("Wrong length of zu.") + cdef np.ndarray[DTYPEd_t, ndim=1] mult_x_L = np.array(zl, dtype=DTYPEd).flatten() cdef np.ndarray[DTYPEd_t, ndim=1] mult_x_U = np.array(zu, dtype=DTYPEd).flatten() @@ -1260,7 +1282,7 @@ cdef Bool intermediate_cb(Index alg_mod, return True ret_val = self.__intermediate(alg_mod, - iter_count, + iter_count, obj_value, inf_pr, inf_du, diff --git a/cyipopt/tests/conftest.py b/cyipopt/tests/conftest.py index 41a1805..f34249a 100644 --- a/cyipopt/tests/conftest.py +++ b/cyipopt/tests/conftest.py @@ -102,9 +102,9 @@ def hessian_structure(x): def hs071_intermediate_fixture(): """Return a function for a default intermediate function.""" def intermediate(*args): - iter_count = args[2] - obj_value = args[3] - msg = f"Objective value at iteration #{iter_count} is - {obj_value}" + iter_count = args[1] + obj_value = args[2] + msg = f"Objective value at iteration #{iter_count} is {obj_value}" print(msg) return intermediate @@ -112,14 +112,14 @@ def intermediate(*args): @pytest.fixture() def hs071_definition_instance_fixture(hs071_objective_fixture, - hs071_gradient_fixture, - hs071_constraints_fixture, - hs071_jacobian_fixture, - hs071_jacobian_structure_fixture, - hs071_hessian_fixture, - hs071_hessian_structure_fixture, - hs071_intermediate_fixture, - ): + hs071_gradient_fixture, + hs071_constraints_fixture, + hs071_jacobian_fixture, + hs071_jacobian_structure_fixture, + hs071_hessian_fixture, + hs071_hessian_structure_fixture, + hs071_intermediate_fixture, + ): """Return a default implementation of the hs071 test problem.""" class hs071: @@ -146,6 +146,14 @@ def hs071_initial_guess_fixture(): return x0 +@pytest.fixture() +def hs071_optimal_solution_fixture(): + """Return the optimal solution for the hs071 test problem.""" + x_opt = [1.0, 4.74299964, 3.82114998, 1.37940829] + f_opt = 17.01401714021362 + return x_opt, f_opt + + @pytest.fixture() def hs071_variable_lower_bounds_fixture(): """Return a default variable lower bounds for the hs071 test problem.""" diff --git a/cyipopt/tests/integration/test_hs071.py b/cyipopt/tests/integration/test_hs071.py index bc64d00..224f863 100644 --- a/cyipopt/tests/integration/test_hs071.py +++ b/cyipopt/tests/integration/test_hs071.py @@ -4,19 +4,53 @@ import cyipopt -def test_hs071_solve(hs071_initial_guess_fixture, hs071_problem_instance_fixture): +def test_hs071_solve(hs071_initial_guess_fixture, + hs071_optimal_solution_fixture, + hs071_problem_instance_fixture): """Test hs071 test problem solves to the correct solution.""" x0 = hs071_initial_guess_fixture nlp = hs071_problem_instance_fixture x, info = nlp.solve(x0) - expected_J = 17.01401714021362 - np.testing.assert_almost_equal(info["obj_val"], expected_J) + expected_x, expected_J = hs071_optimal_solution_fixture - expected_x = np.array([1.0, 4.74299964, 3.82114998, 1.37940829]) + np.testing.assert_almost_equal(info["obj_val"], expected_J) np.testing.assert_allclose(x, expected_x) +def test_hs071_warm_start(hs071_initial_guess_fixture, + hs071_optimal_solution_fixture, + hs071_problem_instance_fixture): + x0 = hs071_initial_guess_fixture + nlp = hs071_problem_instance_fixture + + _, info = nlp.solve(x0) + + x_opt, _ = hs071_optimal_solution_fixture + np.testing.assert_allclose(info['x'], x_opt) + + x_init = info['x'] + lagrange = info['mult_g'] + zl = info['mult_x_L'] + zu = info['mult_x_U'] + + # Set parameters to avoid push the solution + # away from the variable bounds + nlp.add_option('warm_start_init_point', 'yes') + nlp.add_option("warm_start_bound_frac", 1e-6) + nlp.add_option("warm_start_bound_push", 1e-6) + nlp.add_option("warm_start_slack_bound_frac", 1e-6) + nlp.add_option("warm_start_slack_bound_push", 1e-6) + nlp.add_option("warm_start_mult_bound_push", 1e-6) + + _, info = nlp.solve(x_init, + lagrange=lagrange, + zl=zl, + zu=zu) + + np.testing.assert_allclose(info['x'], x_opt) + + def _make_problem(definition, lb, ub, cl, cu): n = len(lb) m = len(cl) @@ -25,9 +59,10 @@ def _make_problem(definition, lb, ub, cl, cu): ) -def _solve_and_assert_correct(problem, x0): +def _solve_and_assert_correct(problem, x0, opt_sol): x, info = problem.solve(x0) - expected_x = np.array([1.0, 4.74299964, 3.82114998, 1.37940829]) + expected_x, _ = opt_sol + expected_x = np.array(expected_x) assert info["status"] == 0 np.testing.assert_allclose(x, expected_x) @@ -40,6 +75,7 @@ def _assert_solve_fails(problem, x0): def test_hs071_objective_eval_error( hs071_initial_guess_fixture, + hs071_optimal_solution_fixture, hs071_problem_instance_fixture, hs071_definition_instance_fixture, hs071_variable_lower_bounds_fixture, @@ -60,6 +96,7 @@ def __call__(self, x): objective_with_error = ObjectiveWithError() x0 = hs071_initial_guess_fixture + opt = hs071_optimal_solution_fixture definition = hs071_definition_instance_fixture definition.objective = objective_with_error definition.intermediate = None @@ -77,7 +114,7 @@ def __call__(self, x): # fail. We will need to (a) update these tests and (b) update the # CyIpoptEvaluationError documentation, possibly with Ipopt version-specific # behavior. - _solve_and_assert_correct(problem, x0) + _solve_and_assert_correct(problem, x0, opt) assert objective_with_error.n_eval_error > 0 @@ -129,6 +166,7 @@ def __call__(self, x): def test_hs071_constraints_eval_error( hs071_initial_guess_fixture, + hs071_optimal_solution_fixture, hs071_problem_instance_fixture, hs071_definition_instance_fixture, hs071_variable_lower_bounds_fixture, @@ -149,6 +187,7 @@ def __call__(self, x): constraints_with_error = ConstraintsWithError() x0 = hs071_initial_guess_fixture + opt = hs071_optimal_solution_fixture definition = hs071_definition_instance_fixture definition.constraints = constraints_with_error definition.intermediate = None @@ -159,7 +198,7 @@ def __call__(self, x): cu = hs071_constraint_upper_bounds_fixture problem = _make_problem(definition, lb, ub, cl, cu) - _solve_and_assert_correct(problem, x0) + _solve_and_assert_correct(problem, x0, opt) assert constraints_with_error.n_eval_error > 0