Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation / tests for warm starting #271

Merged
merged 4 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions cyipopt/cython/ipopt_wrapper.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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, )
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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,
Expand Down
30 changes: 19 additions & 11 deletions cyipopt/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,24 @@ 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


@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:
Expand All @@ -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."""
Expand Down
55 changes: 47 additions & 8 deletions cyipopt/tests/integration/test_hs071.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading