From b29c28ec9719899e8dc88ef4bb79dca41f0835e7 Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 2 Nov 2026 18:17:31 -0500 Subject: [PATCH 1/2] [unopy] Apply the preset when it is set with set_option("preset", ...) The string set_option overload only stored "preset" in the user options and never refreshed the preset options, so optimize() overlaid an empty preset and failed on a missing ingredient option (e.g. inequality_handling_method). set_preset and set_option("preset", ...) now go through a single UnoSolverWrapper::set_preset, matching the C API's uno_set_solver_string_option. Co-Authored-By: Claude Opus 5.5 --- .../Python/cpp_classes/UnoSolverWrapper.cpp | 7 ++++ .../Python/cpp_classes/UnoSolverWrapper.hpp | 1 + .../Python/python_modules/UnoSolver.cpp | 13 +++--- interfaces/Python/tests/test_solvers.py | 41 +++++++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp b/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp index 93bba840c..7d30db6f2 100644 --- a/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp +++ b/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp @@ -56,6 +56,13 @@ namespace uno { Logger::set_stream(std::cout); } + // single entry point for the preset, used by both set_preset and set_option("preset", ...) + void UnoSolverWrapper::set_preset(std::string preset) { + this->validate_preset(preset); + this->user_options.set_string("preset", std::move(preset)); + this->refresh_preset_options(); + } + void UnoSolverWrapper::validate_preset(const std::string& preset) { if (preset != "auto") { Presets::from_string(preset); // throws on an unknown preset diff --git a/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp b/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp index 027f81f8b..3bc5a2034 100644 --- a/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp +++ b/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp @@ -39,6 +39,7 @@ namespace uno { UnoSolverWrapper(); ~UnoSolverWrapper(); + void set_preset(std::string preset); void validate_preset(const std::string& preset); void refresh_preset_options(); diff --git a/interfaces/Python/python_modules/UnoSolver.cpp b/interfaces/Python/python_modules/UnoSolver.cpp index a48286039..fa9e61877 100644 --- a/interfaces/Python/python_modules/UnoSolver.cpp +++ b/interfaces/Python/python_modules/UnoSolver.cpp @@ -69,7 +69,12 @@ namespace uno { const auto type = Options::option_types.find(option_name); if (type != Options::option_types.end()) { // key found if (type->second == OptionType::STRING) { // correct type - solver.user_options.set_string(option_name, std::move(option_value)); + if (option_name == "preset") { // the preset also determines the preset options + solver.set_preset(std::move(option_value)); + } + else { + solver.user_options.set_string(option_name, std::move(option_value)); + } } else { // incorrect type throw py::type_error(option_name + " is not of type string"); @@ -82,11 +87,7 @@ namespace uno { .def("set_logger_stream", &UnoSolverWrapper::set_logger_stream) - .def("set_preset", [](UnoSolverWrapper& solver, std::string preset_name) { - solver.validate_preset(preset_name); - solver.user_options.set_string("preset", std::move(preset_name)); - solver.refresh_preset_options(); - }, py::arg("preset_name")) + .def("set_preset", &UnoSolverWrapper::set_preset, py::arg("preset_name")) .def("set_notify_acceptable_iterate_callback", [](UnoSolverWrapper& solver, NotifyAcceptableIterateCallback callback) { solver.set_notify_acceptable_iterate_callback(std::move(callback)); diff --git a/interfaces/Python/tests/test_solvers.py b/interfaces/Python/tests/test_solvers.py index 29c2d87e9..414ac5f90 100644 --- a/interfaces/Python/tests/test_solvers.py +++ b/interfaces/Python/tests/test_solvers.py @@ -73,3 +73,44 @@ def test_hs001_without_constraints(solver, preset, option, subproblem_solver): assert len(result.constraint_dual_solution) == 0 assert log.counts["hessian"] > 0 and log.counts["notify"] > 0 assert log.multiplier_sizes and all(size == 0 for size in log.multiplier_sizes) + + +PRESETS = ["filtersqp", "filterslp", "funnelsqp", "ipopt", "auto"] + + +@pytest.mark.parametrize("preset", PRESETS) +def test_preset_as_option(preset): + """Setting the preset with set_option("preset", ...) selects the same method and solution as set_preset""" + results = {} + descriptions = {} + for setter in ("set_option", "set_preset"): + solver = unopy.UnoSolver() + solver.set_option("logger", "SILENT") + if setter == "set_option": + solver.set_option("preset", preset) + else: + solver.set_preset(preset) + results[setter] = solver.optimize(hs015()) + descriptions[setter] = solver.get_method_description() + assert descriptions["set_option"] == descriptions["set_preset"] + assert results["set_option"].optimization_status == results["set_preset"].optimization_status + assert results["set_option"].number_iterations == results["set_preset"].number_iterations + assert np.array_equal(results["set_option"].primal_solution, results["set_preset"].primal_solution) + + +def test_preset_option_overrides_previous_preset(solver): + """A preset set with set_option replaces a preset previously set with set_preset""" + solver.set_preset("filtersqp") + solver.set_option("preset", "ipopt") + solver.optimize(hs015()) + assert "interior-point" in solver.get_method_description() + + +@pytest.mark.parametrize("setter", ["set_option", "set_preset"]) +def test_unknown_preset_raises(solver, setter): + """An unknown preset name is rejected when it is set, whether through set_option or set_preset""" + with pytest.raises(RuntimeError, match="Unknown preset"): + if setter == "set_option": + solver.set_option("preset", "not_a_preset") + else: + solver.set_preset("not_a_preset") From 9d74d41ce445d098c360b762600bad3188aa41cf Mon Sep 17 00:00:00 2001 From: Charlie Vanaret Date: Tue, 3 Nov 2026 11:04:43 +0100 Subject: [PATCH 2/2] Apply batched suggestions from code review Co-authored-by: Charlie Vanaret --- interfaces/Python/python_modules/UnoSolver.cpp | 2 +- interfaces/Python/tests/test_solvers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interfaces/Python/python_modules/UnoSolver.cpp b/interfaces/Python/python_modules/UnoSolver.cpp index fa9e61877..49fbb3542 100644 --- a/interfaces/Python/python_modules/UnoSolver.cpp +++ b/interfaces/Python/python_modules/UnoSolver.cpp @@ -69,7 +69,7 @@ namespace uno { const auto type = Options::option_types.find(option_name); if (type != Options::option_types.end()) { // key found if (type->second == OptionType::STRING) { // correct type - if (option_name == "preset") { // the preset also determines the preset options + if (option_name == "preset") { // setting a preset sets the corresponding options solver.set_preset(std::move(option_value)); } else { diff --git a/interfaces/Python/tests/test_solvers.py b/interfaces/Python/tests/test_solvers.py index 414ac5f90..b733e7f3a 100644 --- a/interfaces/Python/tests/test_solvers.py +++ b/interfaces/Python/tests/test_solvers.py @@ -75,7 +75,7 @@ def test_hs001_without_constraints(solver, preset, option, subproblem_solver): assert log.multiplier_sizes and all(size == 0 for size in log.multiplier_sizes) -PRESETS = ["filtersqp", "filterslp", "funnelsqp", "ipopt", "auto"] +PRESETS = ["filtersqp", "ipopt", "auto"] @pytest.mark.parametrize("preset", PRESETS)