Skip to content

Commit

Permalink
Merge pull request #271 from chrhansk/feature-solve-documentation
Browse files Browse the repository at this point in the history
Add documentation / tests for warm starting
  • Loading branch information
moorepants authored Oct 11, 2024
2 parents 72802e4 + 4722cd6 commit a6fcb88
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 23 deletions.
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

0 comments on commit a6fcb88

Please sign in to comment.