From a5ce145bdd47e1ffae5aec479b0e6afbff517cab Mon Sep 17 00:00:00 2001 From: Nickolai Belakovski Date: Fri, 8 Mar 2024 23:05:35 -0800 Subject: [PATCH] Responding to PR comments --- python/_prima.cpp | 10 +- python/prima/__init__.py | 273 ++++++++++++--------- python/prima/_nonlinear_constraints.py | 4 + python/tests/README.md | 1 + python/tests/test_basic_functionality.py | 56 ++--- python/tests/test_combining_constraints.py | 26 +- python/tests/test_compatibility_pdfo.py | 6 +- python/tests/test_compatibility_scipy.py | 6 +- python/tests/test_options.py | 33 ++- 9 files changed, 234 insertions(+), 181 deletions(-) diff --git a/python/_prima.cpp b/python/_prima.cpp index cf4c350eec..a5a799531b 100644 --- a/python/_prima.cpp +++ b/python/_prima.cpp @@ -30,7 +30,7 @@ class SelfCleaningPyObject { struct PRIMAResult { // Construct PRIMAResult from prima_result_t - PRIMAResult(const prima_result_t& result, const int num_vars, const int num_constraints) : + PRIMAResult(const prima_result_t& result, const int num_vars, const int num_constraints, const std::string method) : x(num_vars, result.x), success(prima_is_success(result)), status(result.status), @@ -38,7 +38,8 @@ struct PRIMAResult { fun(result.f), nfev(result.nf), maxcv(result.cstrv), - nlconstr(num_constraints, result.nlconstr) {} + nlconstr(num_constraints, result.nlconstr), + method(method) {} std::string repr() const { std::string repr = "PRIMAResult("; @@ -51,6 +52,7 @@ struct PRIMAResult { "nfev=" + std::to_string(nfev) + ", " + "maxcv=" + std::to_string(maxcv) + ", " + "nlconstr=" + std::string(pybind11::repr(nlconstr)) + + "method=" + "\'" + method + "\'" + ")"; ")"; return repr; } @@ -63,6 +65,7 @@ struct PRIMAResult { int nfev; // number of objective function calls double maxcv; // constraint violation (cobyla & lincoa) pybind11::array_t nlconstr; // non-linear constraint values, of size m_nlcon (cobyla only) + std::string method; // optimization method }; @@ -94,6 +97,7 @@ PYBIND11_MODULE(_prima, m) { .def_readwrite("nfev", &PRIMAResult::nfev) .def_readwrite("maxcv", &PRIMAResult::maxcv) .def_readwrite("nlconstr", &PRIMAResult::nlconstr) + .def_readwrite("method", &PRIMAResult::method) .def("__repr__", &PRIMAResult::repr); py::enum_(m, "PRIMAMessage") @@ -325,7 +329,7 @@ PYBIND11_MODULE(_prima, m) { // Initialize the result, call the function, convert the return type, and return it. prima_result_t result; const prima_rc_t rc = prima_minimize(algorithm, &problem, &options, &result); - PRIMAResult result_copy(result, py_x0.size(), problem.m_nlcon); + PRIMAResult result_copy(result, py_x0.size(), problem.m_nlcon, method.cast()); prima_free_result(&result); return result_copy; }, "fun"_a, "x0"_a, "args"_a=py::tuple(), "method"_a=py::none(), diff --git a/python/prima/__init__.py b/python/prima/__init__.py index 2d0cd18fb0..4535d6d128 100644 --- a/python/prima/__init__.py +++ b/python/prima/__init__.py @@ -1,137 +1,180 @@ from ._prima import minimize as _minimize, __version__, PRIMAMessage from ._nonlinear_constraints import NonlinearConstraint, process_nl_constraints -from ._linear_constraints import LinearConstraint, process_single_linear_constraint, process_multiple_linear_constraints, separate_LC_into_eq_and_ineq +from ._linear_constraints import ( + LinearConstraint, + process_single_linear_constraint, + process_multiple_linear_constraints, + separate_LC_into_eq_and_ineq, +) from ._bounds import process_bounds, Bounds from enum import Enum import numpy as np from ._common import _project - class ConstraintType(Enum): - LINEAR_NATIVE=5 - NONLINEAR_NATIVE=10 - LINEAR_NONNATIVE=15 - NONLINEAR_NONNATIVE=20 - LINEAR_DICT=25 - NONLINEAR_DICT=30 + LINEAR_NATIVE = 5 + NONLINEAR_NATIVE = 10 + LINEAR_NONNATIVE = 15 + NONLINEAR_NONNATIVE = 20 + LINEAR_DICT = 25 + NONLINEAR_DICT = 30 def get_constraint_type(constraint): - # Make sure the test for native is first, since the hasattr tests will also pass for native constraints - if isinstance(constraint, LinearConstraint): return ConstraintType.LINEAR_NATIVE - elif isinstance(constraint, NonlinearConstraint): return ConstraintType.NONLINEAR_NATIVE - elif isinstance(constraint, dict) and ('A' in constraint) and ('lb' in constraint) and ('ub' in constraint): return ConstraintType.LINEAR_DICT - elif isinstance(constraint, dict) and ('fun' in constraint) and ('lb' in constraint) and ('ub' in constraint): return ConstraintType.NONLINEAR_DICT - elif hasattr(constraint, 'A') and hasattr(constraint, 'lb') and hasattr(constraint, 'ub'): return ConstraintType.LINEAR_NONNATIVE - elif hasattr(constraint, 'fun') and hasattr(constraint, 'lb') and hasattr(constraint, 'ub'): return ConstraintType.NONLINEAR_NONNATIVE - else: raise ValueError('Constraint type not recognized') + # Make sure the test for native is first, since the hasattr tests will also pass for native constraints + if isinstance(constraint, LinearConstraint): + return ConstraintType.LINEAR_NATIVE + elif isinstance(constraint, NonlinearConstraint): + return ConstraintType.NONLINEAR_NATIVE + elif isinstance(constraint, dict) and ("A" in constraint) and ("lb" in constraint) and ("ub" in constraint): + return ConstraintType.LINEAR_DICT + elif isinstance(constraint, dict) and ("fun" in constraint) and ("lb" in constraint) and ("ub" in constraint): + return ConstraintType.NONLINEAR_DICT + elif hasattr(constraint, "A") and hasattr(constraint, "lb") and hasattr(constraint, "ub"): + return ConstraintType.LINEAR_NONNATIVE + elif hasattr(constraint, "fun") and hasattr(constraint, "lb") and hasattr(constraint, "ub"): + return ConstraintType.NONLINEAR_NONNATIVE + else: + raise ValueError("Constraint type not recognized") def process_constraints(constraints, x0, options): - # First throw it back if it's None - if constraints is None: - return None, None - # Next figure out if it's a list of constraints or a single constraint - # If it's a single constraint, make it a list, and then the remaining logic - # doesn't have to change - if not isinstance(constraints, list): - constraints = [constraints] - - # Separate out the linear and nonlinear constraints - linear_constraints = [] - nonlinear_constraints = [] - for constraint in constraints: - constraint_type = get_constraint_type(constraint) - if constraint_type in (ConstraintType.LINEAR_NATIVE, ConstraintType.LINEAR_NONNATIVE): - linear_constraints.append(constraint) - elif constraint_type in (ConstraintType.NONLINEAR_NATIVE, ConstraintType.NONLINEAR_NONNATIVE): - nonlinear_constraints.append(constraint) - elif constraint_type == ConstraintType.LINEAR_DICT: - linear_constraints.append(LinearConstraint(constraint['A'], constraint['lb'], constraint['ub'])) - elif constraint_type == ConstraintType.NONLINEAR_DICT: - nonlinear_constraints.append(NonlinearConstraint(constraint['fun'], constraint['lb'], constraint['ub'])) + # First throw it back if it's None + if constraints is None: + return None, None + # Next figure out if it's a list of constraints or a single constraint + # If it's a single constraint, make it a list, and then the remaining logic + # doesn't have to change + if not isinstance(constraints, list): + constraints = [constraints] + + # Separate out the linear and nonlinear constraints + linear_constraints = [] + nonlinear_constraints = [] + for constraint in constraints: + constraint_type = get_constraint_type(constraint) + if constraint_type in ( + ConstraintType.LINEAR_NATIVE, + ConstraintType.LINEAR_NONNATIVE, + ): + linear_constraints.append(constraint) + elif constraint_type in ( + ConstraintType.NONLINEAR_NATIVE, + ConstraintType.NONLINEAR_NONNATIVE, + ): + nonlinear_constraints.append(constraint) + elif constraint_type == ConstraintType.LINEAR_DICT: + linear_constraints.append(LinearConstraint(constraint["A"], constraint["lb"], constraint["ub"])) + elif constraint_type == ConstraintType.NONLINEAR_DICT: + nonlinear_constraints.append(NonlinearConstraint(constraint["fun"], constraint["lb"], constraint["ub"])) + else: + raise ValueError("Constraint type not recognized") + + if len(nonlinear_constraints) > 0: + nonlinear_constraint = process_nl_constraints(x0, nonlinear_constraints, options) + else: + nonlinear_constraint = None + + # Determine if we have multiple linear constraints, just 1, or none, and process accordingly + if len(linear_constraints) > 1: + linear_constraint = process_multiple_linear_constraints(linear_constraints) + elif len(linear_constraints) == 1: + linear_constraint = process_single_linear_constraint(linear_constraints[0]) else: - raise ValueError('Constraint type not recognized') - - if len(nonlinear_constraints) > 0: - nonlinear_constraint = process_nl_constraints(x0, nonlinear_constraints, options) - else: - nonlinear_constraint = None - - # Determine if we have a multiple linear constraints, just 1, or none, and process accordingly - if len(linear_constraints) > 1: - linear_constraint = process_multiple_linear_constraints(linear_constraints) - elif len(linear_constraints) == 1: - linear_constraint = process_single_linear_constraint(linear_constraints[0]) - else: - linear_constraint = None - - return linear_constraint, nonlinear_constraint + linear_constraint = None + + return linear_constraint, nonlinear_constraint + def minimize(fun, x0, args=(), method=None, bounds=None, constraints=None, callback=None, options=None): - temp_options = {} - linear_constraint, nonlinear_constraint = process_constraints(constraints, x0, temp_options) - if options is None: - options = temp_options - else: - options.update(temp_options) + temp_options = {} + linear_constraint, nonlinear_constraint = process_constraints(constraints, x0, temp_options) + if options is None: + options = temp_options + else: + options.update(temp_options) + + quiet = options.get("quiet", True) + + if method is None: + if nonlinear_constraint is not None: + if not quiet: print("Nonlinear constraints detected, applying COBYLA") + method = "cobyla" + elif linear_constraint is not None: + if not quiet: print("Linear constraints detected without nonlinear constraints, applying LINCOA") + method = "lincoa" + elif bounds is not None: + if not quiet: print("Bounds without linear or nonlinear constraints detected, applying BOBYQA") + method = "bobyqa" + else: + if not quiet: print("No bounds or constraints detected, applying NEWUOA") + method = "newuoa" + else: + # Raise some errors if methods were called with inappropriate options + method = method.lower() + if method != "cobyla" and nonlinear_constraint is not None: + raise ValueError("Nonlinear constraints were provided for an algorithm that cannot handle them") + if method not in ("cobyla", "lincoa") and linear_constraint is not None: + raise ValueError("Linear constraints were provided for an algorithm that cannot handle them") + if method not in ("cobyla", "bobyqa", "lincoa") and bounds is not None: + raise ValueError("Bounds were provided for an algorithm that cannot handle them") + + try: + lenx0 = len(x0) + except TypeError: + lenx0 = 1 + + lb, ub = process_bounds(bounds, lenx0) + + if linear_constraint is not None: + # this function doesn't take nonlinear constraints into account at this time + x0 = _project(x0, lb, ub, {"linear": linear_constraint, "nonlinear": None}) + A_eq, b_eq, A_ineq, b_ineq = separate_LC_into_eq_and_ineq(linear_constraint) + else: + A_eq = None + b_eq = None + A_ineq = None + b_ineq = None - if method is None: if nonlinear_constraint is not None: - print("Nonlinear constraints detected, applying COBYLA") - method = "cobyla" - elif linear_constraint is not None: - print("Linear constraints detected without nonlinear constraints, applying LINCOA") - method = "lincoa" - elif bounds is not None: - print("Bounds without linear or nonlinear constraints detected, applying BOBYQA") - method = "bobyqa" + # PRIMA prefers -inf < f(x) <= 0, so we need to modify the nonlinear constraint accordingly + + def constraint_function(x): + values = np.array(nonlinear_constraint.fun(x), dtype=np.float64) + + return np.concatenate( + ( + values - nonlinear_constraint.ub, + [lb_i - vi for lb_i, vi in zip(nonlinear_constraint.lb, values) if lb_i != -np.inf], + ) + ) + + if options is None: + options = {} + options["m_nlcon"] = len(nonlinear_constraint.lb) + len( + [lb_i for lb_i in nonlinear_constraint.lb if lb_i != -np.inf] + ) + options["nlconstr0"] = constraint_function(x0) + options["nlconstr0"] = np.array(options["nlconstr0"], dtype=np.float64) + if "f0" not in options: + options["f0"] = fun(x0) else: - print("No bounds or constraints detected, applying NEWUOA") - method = "newuoa" - else: - # Raise some errors if methods were called with inappropriate options - method = method.lower() - if method != "cobyla" and nonlinear_constraint is not None: - raise ValueError('Nonlinear constraints were provided for an algorithm that cannot handle them') - if method not in ("cobyla", "lincoa") and linear_constraint is not None: - raise ValueError('Linear constraints were provided for an algorithm that cannot handle them') - if method not in ("cobyla", "bobyqa", "lincoa") and bounds is not None: - raise ValueError('Bounds were provided for an algorithm that cannot handle them') - - try: - lenx0 = len(x0) - except TypeError: - lenx0 = 1 - - lb, ub = process_bounds(bounds, lenx0) - - if linear_constraint is not None: - # this function doesn't take nonlinear constraints into account at this time - x0 = _project(x0, lb, ub, {'linear': linear_constraint, 'nonlinear': None}) - A_eq, b_eq, A_ineq, b_ineq = separate_LC_into_eq_and_ineq(linear_constraint) - else: - A_eq = None - b_eq = None - A_ineq = None - b_ineq = None - - if nonlinear_constraint is not None: - # PRIMA prefers -inf < f(x) <= 0, so we need to modify the nonlinear constraint accordingly - - def constraint_function(x): - values = np.array(nonlinear_constraint.fun(x), dtype=np.float64) - - return np.concatenate((values - nonlinear_constraint.ub, [lb_i - vi for lb_i, vi in zip(nonlinear_constraint.lb, values) if lb_i != -np.inf])) - if options is None: - options = {} - options['m_nlcon'] = len(nonlinear_constraint.lb) + len([lb_i for lb_i in nonlinear_constraint.lb if lb_i != -np.inf]) - options['nlconstr0'] = constraint_function(x0) - options['nlconstr0'] = np.array(options['nlconstr0'], dtype=np.float64) - if 'f0' not in options: options['f0'] = fun(x0) - else: - constraint_function = None - - return _minimize(fun, x0, args, method, lb, ub, A_eq, b_eq, A_ineq, b_ineq, constraint_function, callback, options) + constraint_function = None + + return _minimize( + fun, + x0, + args, + method, + lb, + ub, + A_eq, + b_eq, + A_ineq, + b_ineq, + constraint_function, + callback, + options, + ) diff --git a/python/prima/_nonlinear_constraints.py b/python/prima/_nonlinear_constraints.py index 0db82a993b..9fa8092b43 100644 --- a/python/prima/_nonlinear_constraints.py +++ b/python/prima/_nonlinear_constraints.py @@ -60,6 +60,10 @@ def nlc_fun(x): constraints.append(constraints_i) return np.array(constraints) + # TODO: Incorporate the handling of lb/ub into the nlc_fun + # And then return the constraint function. Options dict will contain + # m_nlcon and f0/nlconstr0 + return NonlinearConstraint(nlc_fun, lb=lb, ub=ub) diff --git a/python/tests/README.md b/python/tests/README.md index 79d8b757ab..2a3df99e0c 100644 --- a/python/tests/README.md +++ b/python/tests/README.md @@ -44,6 +44,7 @@ order to run that test by itself. - [x] npt - test_options.py::test_npt - [x] rhobeg - test_options.py::test_rhobeg - [x] rhoend - test_options.py::test_rhoend + - [x] quiet - test_options.py::test_quiet - [x] an objective function without args can be used successfully - test_options.py::test_normal_function - [x] an objective function with args can be used successfully - test_options.py::test_function_with_regular_args - [x] an objective function with *args can be used successfully - test_options.py::test_function_with_star_args diff --git a/python/tests/test_basic_functionality.py b/python/tests/test_basic_functionality.py index 6b7a8cb2cb..7d3e50e51d 100644 --- a/python/tests/test_basic_functionality.py +++ b/python/tests/test_basic_functionality.py @@ -2,98 +2,80 @@ import numpy as np from objective import fun -def test_provide_nonlinear_constraints_alone(capfd): + +def test_provide_nonlinear_constraints_alone(): nlc = NLC(lambda x: np.array([x[0]**2, x[1]**2]), lb=[25]*2, ub=[100]*2) x0 = [0, 0] res = minimize(fun, x0, constraints=nlc) assert np.isclose(res.x[0], 5, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 5, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 1, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == "Nonlinear constraints detected, applying COBYLA\n" - assert outerr.err == '' + assert res.method == "cobyla" -def test_provide_nonlinear_constraints_alone_and_select_COBYLA(capfd): +def test_provide_nonlinear_constraints_alone_and_select_COBYLA(): nlc = NLC(lambda x: np.array([x[0]**2, x[1]**2]), lb=[25]*2, ub=[100]*2) x0 = [0, 0] res = minimize(fun, x0, constraints=nlc, method="COBYLA") assert np.isclose(res.x[0], 5, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 5, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 1, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == '' - assert outerr.err == '' + assert res.method == "cobyla" -def test_provide_linear_constraints_alone(capfd): +def test_provide_linear_constraints_alone(): lc = LC(np.array([[1, 1],[1, -1]]), lb=[0, 0], ub=[8, 2]) x0 = [0, 0] res = minimize(fun, x0, constraints=lc) assert np.isclose(res.x[0], 4.5, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 3.5, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 0.5, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == "Linear constraints detected without nonlinear constraints, applying LINCOA\n" - assert outerr.err == '' - + assert res.method == "lincoa" -def test_provide_linear_constraints_alone_and_select_LINCOA(capfd): +def test_provide_linear_constraints_alone_and_select_LINCOA(): lc = LC(np.array([[1, 1],[1, -1]]), lb=[0, 0], ub=[8, 2]) x0 = [0, 0] res = minimize(fun, x0, constraints=lc, method="LINCOA") assert np.isclose(res.x[0], 4.5, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 3.5, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 0.5, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == '' - assert outerr.err == '' + assert res.method == "lincoa" -def test_provide_bounds_alone(capfd): +def test_provide_bounds_alone(): x0 = [0, 0] res = minimize(fun, x0, bounds=([0, 3], [0, 3])) assert np.isclose(res.x[0], 3, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 3, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 5, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == "Bounds without linear or nonlinear constraints detected, applying BOBYQA\n" - assert outerr.err == '' + assert res.method == "bobyqa" -def test_provide_bounds_alone_and_select_BOBYQA(capfd): +def test_provide_bounds_alone_and_select_BOBYQA(): x0 = [0, 0] res = minimize(fun, x0, bounds=([0, 3], [0, 3]), method="BOBYQA") assert np.isclose(res.x[0], 3, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 3, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 5, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == '' - assert outerr.err == '' + assert res.method == "bobyqa" -def test_not_providing_bounds_linear_constraints_or_nonlinear_constraints(capfd): +def test_not_providing_bounds_linear_constraints_or_nonlinear_constraints(): x0 = [0, 0] res = minimize(fun, x0) assert fun.result_point_and_value_are_optimal(res) - outerr = capfd.readouterr() - assert outerr.out == "No bounds or constraints detected, applying NEWUOA\n" - assert outerr.err == '' + assert res.method == "newuoa" -def test_not_providing_bounds_linear_constraints_or_nonlinear_constraints_and_select_NEWUOA(capfd): +def test_not_providing_bounds_linear_constraints_or_nonlinear_constraints_and_select_NEWUOA(): x0 = [0, 0] res = minimize(fun, x0, method="NEWUOA") assert fun.result_point_and_value_are_optimal(res) - outerr = capfd.readouterr() - assert outerr.out == '' - assert outerr.err == '' + assert res.method == "newuoa" -def test_not_providing_bounds_linear_constraints_or_nonlinear_constraints_and_select_UOBYQA(capfd): +def test_not_providing_bounds_linear_constraints_or_nonlinear_constraints_and_select_UOBYQA(): x0 = [0, 0] res = minimize(fun, x0, method="UOBYQA") assert fun.result_point_and_value_are_optimal(res) - outerr = capfd.readouterr() - assert outerr.out == '' - assert outerr.err == '' + assert res.method == "uobyqa" diff --git a/python/tests/test_combining_constraints.py b/python/tests/test_combining_constraints.py index 05382babe7..c9a6a9899b 100644 --- a/python/tests/test_combining_constraints.py +++ b/python/tests/test_combining_constraints.py @@ -3,7 +3,7 @@ from objective import fun -def test_providing_linear_and_nonlinear_constraints(capfd): +def test_providing_linear_and_nonlinear_constraints(): nlc = prima_NLC(lambda x: x[0]**2, lb=[25], ub=[100]) lc = prima_LC(np.array([1,1]), lb=10, ub=15) x0 = [0, 0] @@ -11,12 +11,10 @@ def test_providing_linear_and_nonlinear_constraints(capfd): assert np.isclose(res.x[0], 5.5, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 4.5, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 0.5, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == "Nonlinear constraints detected, applying COBYLA\n" - assert outerr.err == '' + assert res.method == "cobyla" -def test_providing_bounds_and_linear_constraints(capfd): +def test_providing_bounds_and_linear_constraints(): lc = prima_LC(np.array([1,1]), lb=10, ub=15) bounds = prima_Bounds(1, 1) x0 = [0, 0] @@ -24,12 +22,10 @@ def test_providing_bounds_and_linear_constraints(capfd): assert np.isclose(res.x[0], 1, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 9, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 41, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == "Linear constraints detected without nonlinear constraints, applying LINCOA\n" - assert outerr.err == '' + assert res.method == "lincoa" -def test_providing_bounds_and_nonlinear_constraints(capfd): +def test_providing_bounds_and_nonlinear_constraints(): nlc = prima_NLC(lambda x: x[0]**2, lb=[25], ub=[100]) bounds = prima_Bounds([None, 1], [None, 1]) x0 = [0, 0] @@ -37,14 +33,12 @@ def test_providing_bounds_and_nonlinear_constraints(capfd): assert np.isclose(res.x[0], 5, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[1], 1, atol=1e-6, rtol=1e-6) assert np.isclose(res.fun, 9, atol=1e-6, rtol=1e-6) - outerr = capfd.readouterr() - assert outerr.out == "Nonlinear constraints detected, applying COBYLA\n" - assert outerr.err == '' + assert res.method == "cobyla" # This test is re-used for the compatibility tests, hence the extra arguments and their # default values -def test_providing_bounds_and_linear_and_nonlinear_constraints(capfd, minimize=prima_minimize, NLC=prima_NLC, LC=prima_LC, Bounds=prima_Bounds, package='prima'): +def test_providing_bounds_and_linear_and_nonlinear_constraints(minimize=prima_minimize, NLC=prima_NLC, LC=prima_LC, Bounds=prima_Bounds, package='prima'): # This test needs a 3 variable objective function so that we can check that the # bounds and constraints are all active def newfun(x): @@ -79,7 +73,5 @@ def newfun(x): assert np.isclose(res.x[1], 1, atol=1e-6, rtol=1e-6) assert np.isclose(res.x[2], 3.5, atol=1e-3, rtol=1e-3) assert np.isclose(res.fun, 9.5, atol=1e-3, rtol=1e-3) - if package == 'prima': - outerr = capfd.readouterr() - assert outerr.out == "Nonlinear constraints detected, applying COBYLA\n" - assert outerr.err == '' + if package == 'prima' or package == 'pdfo': + assert res.method == "cobyla" diff --git a/python/tests/test_compatibility_pdfo.py b/python/tests/test_compatibility_pdfo.py index a318bbb32b..6eaaaf6cd5 100644 --- a/python/tests/test_compatibility_pdfo.py +++ b/python/tests/test_compatibility_pdfo.py @@ -5,9 +5,9 @@ from test_combining_constraints import test_providing_bounds_and_linear_and_nonlinear_constraints -def test_prima(capfd): +def test_prima(): from prima import minimize, NonlinearConstraint as NLC, LinearConstraint as LC, Bounds - test_providing_bounds_and_linear_and_nonlinear_constraints(capfd, minimize, NLC, LC, Bounds) + test_providing_bounds_and_linear_and_nonlinear_constraints(minimize, NLC, LC, Bounds) # Despite the fact that we are using the pdfo function, we still get this warning because the pdfo @@ -23,4 +23,4 @@ def test_pdfo(): from pdfo import pdfo from scipy.optimize import NonlinearConstraint as NLC, LinearConstraint as LC, Bounds - test_providing_bounds_and_linear_and_nonlinear_constraints(None, pdfo, NLC, LC, Bounds, package='pdfo') + test_providing_bounds_and_linear_and_nonlinear_constraints(pdfo, NLC, LC, Bounds, package='pdfo') diff --git a/python/tests/test_compatibility_scipy.py b/python/tests/test_compatibility_scipy.py index cae4506449..459cf969f9 100644 --- a/python/tests/test_compatibility_scipy.py +++ b/python/tests/test_compatibility_scipy.py @@ -5,9 +5,9 @@ from test_combining_constraints import test_providing_bounds_and_linear_and_nonlinear_constraints -def test_prima(capfd): +def test_prima(): from prima import minimize, NonlinearConstraint as NLC, LinearConstraint as LC, Bounds - test_providing_bounds_and_linear_and_nonlinear_constraints(capfd, minimize, NLC, LC, Bounds) + test_providing_bounds_and_linear_and_nonlinear_constraints(minimize, NLC, LC, Bounds) def test_scipy(): @@ -15,4 +15,4 @@ def test_scipy(): if version.parse(scipy.__version__) < version.parse("1.11.0"): pytest.skip("scipy version too old for this test (its version of COBYLA does not accept bounds)") from scipy.optimize import minimize, NonlinearConstraint as NLC, LinearConstraint as LC, Bounds - test_providing_bounds_and_linear_and_nonlinear_constraints(None, minimize, NLC, LC, Bounds, package="scipy") + test_providing_bounds_and_linear_and_nonlinear_constraints(minimize, NLC, LC, Bounds, package="scipy") diff --git a/python/tests/test_options.py b/python/tests/test_options.py index d1e945ba5c..1b195ab85b 100644 --- a/python/tests/test_options.py +++ b/python/tests/test_options.py @@ -1,4 +1,4 @@ -from prima import minimize, NonlinearConstraint as NLC, PRIMAMessage +from prima import minimize, LinearConstraint as LC, NonlinearConstraint as NLC, PRIMAMessage from objective import fun import numpy as np import pytest @@ -73,8 +73,7 @@ def test_iprint(capfd): res = minimize(fun, x0, options=options) assert fun.result_point_and_value_are_optimal(res) outerr = capfd.readouterr() - assert outerr.out == '''No bounds or constraints detected, applying NEWUOA - + assert outerr.out == ''' Return from NEWUOA because the trust region radius reaches its lower bound. Number of function values = 23 Least value of F = 0.000000000000000E+000 The corresponding X is: 5.000000000000000E+000 4.000000000000000E+000 @@ -122,3 +121,31 @@ def test_rhoend(): # With a smaller rhoend we have a larger tolerance for the final # trust region radius, and so we should converge more quickly assert res_with_rhoend.nfev < res_without_rhoend.nfev + + +@pytest.mark.parametrize("method", ["NEWUOA", "BOBYQA", "LINCOA", "COBYLA"]) +def test_quiet(capfd, method): + nlc = NLC(lambda x: np.array([x[0]**2, x[1]**2]), lb=[25]*2, ub=[100]*2) + lc = LC(np.array([[1, 1],[1, -1]]), lb=[0, 0], ub=[8, 2]) + bounds = ([0, 3], [0, 3]) + x0 = [0.0] * 2 + options = {'quiet': False} + if method == "NEWUOA": + res = minimize(fun, x0, options=options) + elif method == "BOBYQA": + res = minimize(fun, x0, bounds=bounds, options=options) + elif method == "LINCOA": + res = minimize(fun, x0, constraints=lc, options=options) + elif method == "COBYLA": + res = minimize(fun, x0, constraints=nlc, options=options) + outerr = capfd.readouterr() + if method == "NEWUOA": + assert outerr.out == "No bounds or constraints detected, applying NEWUOA\n" + elif method == "BOBYQA": + assert outerr.out == "Bounds without linear or nonlinear constraints detected, applying BOBYQA\n" + elif method == "LINCOA": + assert outerr.out == "Linear constraints detected without nonlinear constraints, applying LINCOA\n" + elif method == "COBYLA": + assert outerr.out == "Nonlinear constraints detected, applying COBYLA\n" + assert outerr.err == '' + \ No newline at end of file