diff --git a/.github/julia/check_auto_preset.jl b/.github/julia/check_auto_preset.jl new file mode 100644 index 000000000..075fb1a9b --- /dev/null +++ b/.github/julia/check_auto_preset.jl @@ -0,0 +1,85 @@ +# Copyright (c) 2026: Charlie Vanaret and contributors +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +using UnoSolver, JuMP + +function test_hs015() + model = Model(() -> UnoSolver.Optimizer(; logger = "INFO")) + + @variable(model, x, start = -2.) + @variable(model, y, start = 1.) + + @objective(model, Min, 100*(y - x^2)^2 + (1 - x)^2) + + c1 = @constraint(model, x*y >= 1) + c2 = @constraint(model, x + y^2 >= 0) + c3 = @constraint(model, x <= 0.5) + + optimize!(model) + + tolerance = 1e-6 + @assert abs(objective_value(model) - 306.5) <= tolerance + @assert abs(value(x) - 0.5) <= tolerance + @assert abs(value(y) - 2.) <= tolerance + @assert abs(dual(c1) - 700.) <= tolerance + @assert abs(dual(c2) - 0.) <= tolerance + @assert abs(dual(c3) - (-1751.)) <= tolerance + # check the preset + optimizer = unsafe_backend(model) + uno_method = uno_get_method_description(optimizer.solver) + @assert occursin("SQP", uno_method) +end + +function test_camshape_6400() + model = Model(() -> UnoSolver.Optimizer(; logger = "INFO")) + + n = 6400 # number of discretization points + R_v = 1.0 # design parameter related to the valve shape + R_min = 1.0 # minimum allowed radius of the cam + R_max = 2.0 # maximum allowed radius of the cam + alpha = 1.5 # curvature limit parameter + d_theta = 2 * pi / (5 * (n + 1)) # angle between discretization points + cos_dt = cos(d_theta) # constant: d_theta is a parameter, not a variable + + # radius of the cam at discretization points; start at the circle of radius (R_min+R_max)/2 + @variable(model, R_min <= r[1:n] <= R_max, start = (R_min + R_max) / 2) + + @objective(model, Max, (pi * R_v / n) * sum(r[i] for i in 1:n)) + + # Convexity (bilinear in r; cos_dt is a precomputed constant) + @constraint(model, convexity[i in 2:n-1], + -r[i-1] * r[i] - r[i] * r[i+1] + 2 * r[i-1] * r[i+1] * cos_dt <= 0) + @constraint(model, convex_edge1, + -R_min * r[1] - r[1] * r[2] + 2 * R_min * r[2] * cos_dt <= 0) + @constraint(model, convex_edge2, + -R_min^2 - R_min * r[1] + 2 * R_min * r[1] * cos_dt <= 0) + @constraint(model, convex_edge3, + -r[n-1] * r[n] - r[n] * R_max + 2 * r[n-1] * R_max * cos_dt <= 0) + @constraint(model, convex_edge4, + -2 * R_max * r[n] + 2 * r[n]^2 * cos_dt <= 0) + + # Curvature, lower bound + @constraint(model, curvature[i in 1:n-1], -alpha * d_theta <= r[i+1] - r[i]) + @constraint(model, curvature_edge1, -alpha * d_theta <= r[1] - R_min) + @constraint(model, curvature_edge2, -alpha * d_theta <= R_max - r[n]) + + # Curvature, upper bound + @constraint(model, curvature1[i in 1:n-1], r[i+1] - r[i] <= alpha * d_theta) + @constraint(model, curvature_edge11, r[1] - R_min <= alpha * d_theta) + @constraint(model, curvature_edge21, R_max - r[n] <= alpha * d_theta) + + + optimize!(model) + + tolerance = 1e-6 + @assert abs(objective_value(model) - 4.448378) <= tolerance + # check the preset + optimizer = unsafe_backend(model) + uno_method = uno_get_method_description(optimizer.solver) + @assert occursin("interior-point", uno_method) +end + +test_hs015() +test_camshape_6400() \ No newline at end of file diff --git a/.github/workflows/julia-tests-unosolver.yml b/.github/workflows/julia-tests-unosolver.yml index 6d1005daf..1ecb02be9 100644 --- a/.github/workflows/julia-tests-unosolver.yml +++ b/.github/workflows/julia-tests-unosolver.yml @@ -36,6 +36,7 @@ jobs: steps: - uses: actions/checkout@v4 + # Install Julia 1.7 for BinaryBuilder. Note that this is an old version of # Julia, but it is required for compatibility with BinaryBuilder. - uses: julia-actions/setup-julia@v2 @@ -65,6 +66,7 @@ jobs: run: | julia --color=yes -e 'using Pkg; Pkg.add("BinaryBuilder")' julia --color=yes .github/julia/build_tarballs_yggdrasil.jl x86_64-linux-gnu-cxx11 --verbose --deploy="local" + # Now install a newer version of Julia to actually test Uno_jll.jl. # We choose v1.10 because it is the current Long-Term Support (LTS). - uses: julia-actions/setup-julia@v2 @@ -81,4 +83,5 @@ jobs: Pkg.instantiate() Pkg.develop(path="/home/runner/.julia/dev/Uno_jll") Pkg.develop(path="/home/runner/work/Uno/Uno/interfaces/Julia") + include("/home/runner/work/Uno/Uno/.github/julia/check_auto_preset.jl") Pkg.test("UnoSolver") diff --git a/interfaces/AMPL/uno_ampl.cpp b/interfaces/AMPL/uno_ampl.cpp index 084965149..6186523fd 100644 --- a/interfaces/AMPL/uno_ampl.cpp +++ b/interfaces/AMPL/uno_ampl.cpp @@ -21,8 +21,7 @@ void* operator new(size_t size) { */ namespace uno { - void run_uno_ampl(const std::string& model_name, Options& options) { - const AMPLModel model(model_name); + void run_uno_ampl(const AMPLModel& model, Options& options) { Uno uno{}; Result result = uno.solve(model, options); if (options.get_bool("write_solution_to_file")) { @@ -49,12 +48,11 @@ int main(int argc, char* argv[]) { // AMPL expects: ./uno_ampl model.nl [-AMPL] [option_name=option_value, ...] // model name const char* model_name = argv[1]; + const AMPLModel model(model_name); - // gather the options + // set the default options Options options; DefaultOptions::load(options); - // set default preset - Presets::set_default(options); // the -AMPL flag indicates that the solution should be written to the AMPL solution file size_t offset = 2; @@ -68,37 +66,39 @@ int main(int argc, char* argv[]) { try { // get the command line arguments (options start at index offset) const auto command_line_options = Options::get_command_line_options(argc, argv, offset); + + // [optional] read an option file std::optional optional_option_file{}; - std::optional optional_preset{}; for (const auto& [option_name, option_value]: command_line_options) { if (option_name == "option_file") { optional_option_file = option_value; } - else if (option_name == "preset") { - optional_preset = option_value; - } } - - // [optional] set options from an option file if (optional_option_file.has_value()) { Options::load_option_file(options, *optional_option_file); } - - // [optional] set a preset - if (optional_preset.has_value()) { - Presets::set(options, *optional_preset); + for (const auto& [option_name, option_value]: command_line_options) { + if (option_name == "preset") { + options.set_string("preset", option_value); + } + else if (option_name == "logger") { + options.set_string("logger", option_value); + } } + Logger::set_logger(options.get_string("logger")); + + // set the preset (default: auto) + Presets::set(model, options, options.get_string("preset")); - // set the rest of the command line options (note: we add preset to the options for debugging purposes) + // set the rest of the command line options for (const auto& [option_name, option_value]: command_line_options) { - if (option_name != "option_file") { + if (option_name != "option_file" && option_name != "preset" && option_name != "logger") { options.set(option_name, option_value); } } // solve the model - Logger::set_logger(options.get_string("logger")); - run_uno_ampl(model_name, options); + run_uno_ampl(model, options); } catch (const std::exception& e) { std::cout << "uno_ampl failed with the following error: " << e.what() << '\n'; diff --git a/interfaces/C/Uno_C_API.cpp b/interfaces/C/Uno_C_API.cpp index d2d956f60..8edfde781 100644 --- a/interfaces/C/Uno_C_API.cpp +++ b/interfaces/C/Uno_C_API.cpp @@ -453,7 +453,7 @@ COStream* c_ostream = nullptr; struct Solver { Uno* solver; - Options* options; + Options* user_options; UserCallbacks* user_callbacks; Result* result; }; @@ -836,8 +836,6 @@ void* uno_create_solver() { // default options Options* options = new Options; DefaultOptions::load(*options); - // set default preset - Presets::set_default(*options); // default user callbacks UserCallbacks* user_callbacks = new NoUserCallbacks; @@ -854,7 +852,7 @@ bool uno_set_solver_integer_option(void* solver, const char* option_name, uno_in return false; } Solver* uno_solver = static_cast(solver); - uno_solver->options->set_integer(option_name, option_value); + uno_solver->user_options->set_integer(option_name, option_value); return true; } @@ -864,7 +862,7 @@ bool uno_set_solver_double_option(void* solver, const char* option_name, double return false; } Solver* uno_solver = static_cast(solver); - uno_solver->options->set_double(option_name, option_value); + uno_solver->user_options->set_double(option_name, option_value); return true; } @@ -874,7 +872,7 @@ bool uno_set_solver_bool_option(void* solver, const char* option_name, bool opti return false; } Solver* uno_solver = static_cast(solver); - uno_solver->options->set_bool(option_name, option_value); + uno_solver->user_options->set_bool(option_name, option_value); return true; } @@ -883,19 +881,9 @@ bool uno_set_solver_string_option(void* solver, const char* option_name, const c WARNING << "Please specify a valid solver." << std::endl; return false; } - // handle the preset separately - if (strcmp(option_name, "preset") == 0) { - return uno_set_solver_preset(solver, option_value); - } - // handle the option_file separately - else if (strcmp(option_name, "option_file") == 0) { - return uno_load_solver_option_file(solver, option_value); - } - else { - Solver* uno_solver = static_cast(solver); - uno_solver->options->set_string(option_name, option_value); - return true; - } + Solver* uno_solver = static_cast(solver); + uno_solver->user_options->set_string(option_name, option_value); + return true; } uno_int uno_get_solver_option_type(void* solver, const char* option_name) { @@ -905,7 +893,7 @@ uno_int uno_get_solver_option_type(void* solver, const char* option_name) { } Solver* uno_solver = static_cast(solver); try { - return static_cast(uno_solver->options->get_option_type(option_name)); + return static_cast(uno_solver->user_options->get_option_type(option_name)); } catch(const std::out_of_range&) { return UNO_OPTION_TYPE_NOT_FOUND; @@ -974,8 +962,7 @@ bool uno_load_solver_option_file(void* solver, const char* file_name) { return false; } Solver* uno_solver = static_cast(solver); - // load_option_file() handles a possible preset separately - uno::Options::load_option_file(*uno_solver->options, file_name); + Options::load_option_file(*uno_solver->user_options, file_name); return true; } @@ -985,7 +972,7 @@ bool uno_set_solver_preset(void* solver, const char* preset_name) { return false; } Solver* uno_solver = static_cast(solver); - Presets::set(*uno_solver->options, preset_name); + Presets::set(*uno_solver->user_options, preset_name); return true; } @@ -994,7 +981,8 @@ bool uno_set_solver_callbacks(void* solver, uno_notify_acceptable_iterate_callba if (solver == nullptr) { WARNING << "Please specify a valid solver." << std::endl; return false; - } Solver* uno_solver = static_cast(solver); + } + Solver* uno_solver = static_cast(solver); delete uno_solver->user_callbacks; // delete the previous callbacks uno_solver->user_callbacks = new CUserCallbacks(notify_acceptable_iterate_callback, termination_callback, user_data); return true; @@ -1028,13 +1016,21 @@ void uno_optimize(void* solver, void* model) { } Solver* uno_solver = static_cast(solver); - // create an instance of UnoModel, a subclass of Model, and solve the model using Uno + // create an instance of UnoModel, a subclass of Model const UnoModel uno_model(*user_model); - Logger::set_logger(uno_solver->options->get_string("logger")); - Result result = uno_solver->solver->solve(uno_model, *uno_solver->options, *uno_solver->user_callbacks); - // clean up the previous result (if any) + Logger::set_logger(uno_solver->user_options->get_string("logger")); + + // set the preset (default: auto) and gather the options starting from the preset + Options full_options; + Presets::set(uno_model, full_options, uno_solver->user_options->get_string("preset")); + + // copy the rest of the options + full_options.overwrite(*uno_solver->user_options); + + // solve the model + Result result = uno_solver->solver->solve(uno_model, full_options, *uno_solver->user_callbacks); + // clean up the previous result (if any) and move the new result into uno_solver delete uno_solver->result; - // move the new result into uno_solver uno_solver->result = new Result(std::move(result)); // flush the logger Logger::flush(); @@ -1053,7 +1049,7 @@ double uno_get_solver_double_option(void* solver, const char* option_name) { throw std::runtime_error("Please specify a valid solver."); } Solver* uno_solver = static_cast(solver); - return uno_solver->options->get_double(option_name); + return uno_solver->user_options->get_double(option_name); } uno_int uno_get_solver_integer_option(void* solver, const char* option_name) { @@ -1061,7 +1057,7 @@ uno_int uno_get_solver_integer_option(void* solver, const char* option_name) { throw std::runtime_error("Please specify a valid solver."); } Solver* uno_solver = static_cast(solver); - return uno_solver->options->get_int(option_name); + return uno_solver->user_options->get_int(option_name); } bool uno_get_solver_bool_option(void* solver, const char* option_name) { @@ -1069,7 +1065,7 @@ bool uno_get_solver_bool_option(void* solver, const char* option_name) { throw std::runtime_error("Please specify a valid solver."); } Solver* uno_solver = static_cast(solver); - return uno_solver->options->get_bool(option_name); + return uno_solver->user_options->get_bool(option_name); } const char* uno_get_solver_string_option(void* solver, const char* option_name) { @@ -1080,12 +1076,14 @@ const char* uno_get_solver_string_option(void* solver, const char* option_name) // handle the preset and option_file separately if (strcmp(option_name, "option_file") == 0 || strcmp(option_name, "preset") == 0) { try { - return uno_solver->options->get_string(option_name).c_str(); - } catch(const std::out_of_range&) { + return uno_solver->user_options->get_string(option_name).c_str(); + } + catch(const std::out_of_range&) { return nullptr; } - } else { - return uno_solver->options->get_string(option_name).c_str(); + } + else { + return uno_solver->user_options->get_string(option_name).c_str(); } } @@ -1260,7 +1258,7 @@ void uno_destroy_solver(void* solver) { if (solver != nullptr) { Solver* uno_solver = static_cast(solver); delete uno_solver->solver; - delete uno_solver->options; + delete uno_solver->user_options; delete uno_solver->user_callbacks; if (uno_solver->result != nullptr) { delete uno_solver->result; diff --git a/interfaces/Julia/test/MOI_wrapper.jl b/interfaces/Julia/test/MOI_wrapper.jl index 1b93b2dc0..39dba38e1 100644 --- a/interfaces/Julia/test/MOI_wrapper.jl +++ b/interfaces/Julia/test/MOI_wrapper.jl @@ -24,7 +24,7 @@ end function test_MOI_Test() model = MOI.instantiate( - UnoSolver.Optimizer; + () -> UnoSolver.Optimizer(preset="filtersqp"); with_cache_type = Float64, with_bridge_type = Float64, ) @@ -59,7 +59,7 @@ function test_MOI_Test() end function test_Name() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") @test MOI.supports(model, MOI.Name()) @test MOI.get(model, MOI.Name()) == "" MOI.set(model, MOI.Name(), "Model") @@ -68,7 +68,7 @@ function test_Name() end function test_ConstraintDualStart() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 2) c = MOI.add_constraint( @@ -92,7 +92,7 @@ function test_ConstraintDualStart() end function test_ConstraintDualStart_ScalarNonlinearFunction() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 2) MOI.add_constraint.(model, x, MOI.Interval(0.0, 0.8)) @@ -114,7 +114,7 @@ function test_ConstraintDualStart_ScalarNonlinearFunction() end function test_solve_time() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) MOI.add_variable(model) @test isnan(MOI.get(model, MOI.SolveTimeSec())) @@ -124,7 +124,7 @@ function test_solve_time() end function test_barrier_iterations() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) f = (x - 1.0)^2 + 2.0 * x + 3.0 @@ -151,7 +151,7 @@ function MOI.eval_constraint_jacobian(::Issue136, J, x) end function test_empty_optimize() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) @test MOI.get(model, MOI.RawStatusString()) == "Optimize not called" MOI.optimize!(model) @@ -188,7 +188,7 @@ function test_get_model() 3.0 * z * z + z == 27.0 """, ) - uno = UnoSolver.Optimizer() + uno = UnoSolver.Optimizer(preset="filtersqp") index_map = MOI.copy_to(uno, model) attr = MOI.ListOfConstraintTypesPresent() @test sort(MOI.get(model, attr); by = string) == @@ -225,7 +225,7 @@ function test_get_model() end function test_parameter_number_of_variables() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) @test MOI.get(model, MOI.NumberOfVariables()) == 2 @@ -233,7 +233,7 @@ function test_parameter_number_of_variables() end function test_parameter_is_valid() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) @test MOI.is_valid(model, p) @test MOI.is_valid(model, ci) @@ -243,12 +243,12 @@ function test_parameter_is_valid() end function test_parameter_list_of_variable_indices() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) @test MOI.get(model, MOI.ListOfVariableIndices()) == [x, p] # Now reversed - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) x = MOI.add_variable(model) @test MOI.get(model, MOI.ListOfVariableIndices()) == [p, x] @@ -256,7 +256,7 @@ function test_parameter_list_of_variable_indices() end function test_scalar_nonlinear_function_is_valid() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) F, S = MOI.ScalarNonlinearFunction, MOI.EqualTo{Float64} @test MOI.is_valid(model, MOI.ConstraintIndex{F,S}(1)) == false @@ -268,7 +268,7 @@ function test_scalar_nonlinear_function_is_valid() end function test_scalar_nonlinear_function_nlp_block() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) f = MOI.ScalarNonlinearFunction(:^, Any[x, 4]) @@ -281,7 +281,7 @@ function test_scalar_nonlinear_function_nlp_block() end function test_parameter() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) p, ci = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) x = MOI.add_variable(model) @@ -302,7 +302,7 @@ function test_parameter() end function test_parameter_replace_parameters() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) p, ci = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) x = MOI.add_variable(model) @@ -330,7 +330,7 @@ function test_parameter_replace_parameters() end function test_parameter_reverse() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) p, ci = MOI.add_constrained_variable(model, MOI.Parameter(1.0)) @@ -351,7 +351,7 @@ function test_parameter_reverse() end function test_parameter_scalar_affine_objective() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) @@ -378,7 +378,7 @@ end # Uno is dead-lock in this test! # function test_parameter_variable_index_objective() -# model = UnoSolver.Optimizer() +# model = UnoSolver.Optimizer(preset="filtersqp") # MOI.set(model, MOI.Silent(), true) # x = MOI.add_variable(model) # p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) @@ -401,7 +401,7 @@ end # end function test_ListOfSupportedNonlinearOperators() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") ops = MOI.get(model, MOI.ListOfSupportedNonlinearOperators()) @test ops isa Vector{Symbol} @test :|| in ops @@ -415,7 +415,7 @@ function test_ListOfSupportedNonlinearOperators() end function test_ad_backend() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variable(model) attr = MOI.AutomaticDifferentiationBackend() @@ -435,14 +435,14 @@ end function test_mixing_new_old_api() # new then old - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.add_constrained_variable(model, MOI.Parameter(2.0)) bounds = MOI.NLPBoundsPair.([25.0, 40.0], [Inf, 40.0]) block_data = MOI.NLPBlockData(bounds, MOI.Test.HS071(true), true) err = ErrorException("Cannot mix the new and legacy nonlinear APIs") @test_throws err MOI.set(model, MOI.NLPBlock(), block_data) # old then new - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") bounds = MOI.NLPBoundsPair.([25.0, 40.0], [Inf, 40.0]) block_data = MOI.NLPBlockData(bounds, MOI.Test.HS071(true), true) MOI.set(model, MOI.NLPBlock(), block_data) @@ -452,7 +452,7 @@ function test_mixing_new_old_api() end function test_nlp_model_objective_function_type() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) f = MOI.ScalarNonlinearFunction(:sqrt, Any[x]) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) @@ -463,7 +463,7 @@ function test_nlp_model_objective_function_type() end function test_nlp_model_set_set() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) f = MOI.ScalarNonlinearFunction(:sqrt, Any[x]) c = MOI.add_constraint(model, f, MOI.LessThan(2.0)) @@ -475,7 +475,7 @@ end function test_VariablePrimalStart() attr = MOI.VariablePrimalStart() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) @test MOI.supports(model, attr, typeof(x)) @test MOI.get(model, attr, x) === nothing @@ -494,7 +494,7 @@ function test_VariablePrimalStart() end function test_RawOptimizerAttribute() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") attr = MOI.RawOptimizerAttribute("print_solution") @test_throws MOI.GetAttributeNotAllowed{typeof(attr)} MOI.get(model, attr) @test MOI.supports(model, attr) @@ -504,7 +504,7 @@ function test_RawOptimizerAttribute() end function test_empty_nlp_evaluator() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") block = MOI.get(model, MOI.NLPBlock()) evaluator = block.evaluator @test MOI.features_available(evaluator) == [:Grad, :Jac, :Hess, :JacVec, :HessVec] @@ -520,7 +520,7 @@ function test_empty_nlp_evaluator() end function test_NLPBlockDualStart() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 4) MOI.set.(model, MOI.VariablePrimalStart(), x, 1.0) @@ -537,7 +537,7 @@ function test_NLPBlockDualStart() end function test_function_type_to_func() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) MOI.set(model, MOI.ObjectiveFunction{MOI.VariableIndex}(), x) @@ -546,7 +546,7 @@ function test_function_type_to_func() end function test_error_adding_option() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) name, value = "print_solution", :yes MOI.set(model, MOI.RawOptimizerAttribute(name), value) @@ -561,7 +561,7 @@ function test_error_adding_option() end function test_scalar_nonlinear_function_attributes() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variable(model) F, S = MOI.ScalarNonlinearFunction, MOI.LessThan{Float64} @test isempty(MOI.get(model, MOI.ListOfConstraintTypesPresent())) @@ -602,7 +602,7 @@ function test_vector_nonlinear_oracle_two() return end, ) - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 3) MOI.add_constraints.(model, x, MOI.EqualTo.(1.0:3.0)) @@ -659,7 +659,7 @@ function test_vector_nonlinear_oracle_optimization() return end, ) - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 4) t = MOI.add_variable(model) @@ -720,7 +720,7 @@ function test_vector_nonlinear_oracle_optimization_min_sense() return end, ) - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 4) t = MOI.add_variable(model) @@ -764,7 +764,7 @@ function test_vector_nonlinear_oracle_optimization_min_sense() end function test_vector_nonlinear_oracle_scalar_nonlinear_equivalent() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 4) t = MOI.add_variable(model) @@ -820,7 +820,7 @@ function test_vector_nonlinear_oracle_no_hessian() return end, ) - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 3) MOI.add_constraints.(model, x, MOI.EqualTo.(1.0:3.0)) @@ -834,7 +834,7 @@ function test_vector_nonlinear_oracle_no_hessian() end function test_issue_491() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") x = MOI.add_variables(model, 2) g = [MOI.ScalarNonlinearFunction(:log, Any[x[i]]) for i in 1:2] c = MOI.add_constraint.(model, g, MOI.LessThan(1.0)) @@ -846,7 +846,7 @@ function test_issue_491() end function test_issue_491_model_example() - model = MOI.instantiate(UnoSolver.Optimizer; with_cache_type = Float64) + model = MOI.instantiate(() -> UnoSolver.Optimizer(preset="filtersqp"); with_cache_type = Float64) MOI.set(model, MOI.Silent(), true) x = MOI.add_variables(model, 2) MOI.add_constraint.(model, x, MOI.Interval(1.0, 4.0)) @@ -868,7 +868,7 @@ function test_issue_491_model_example() end function test_default_objective_function() - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") F = MOI.get(model, MOI.ObjectiveFunctionType()) @test F == MOI.ScalarAffineFunction{Float64} f = MOI.get(model, MOI.ObjectiveFunction{F}()) @@ -878,7 +878,7 @@ end function test_comprehensive_duals_variable_bounds() model = MOI.instantiate( - UnoSolver.Optimizer; + () -> UnoSolver.Optimizer(preset="filtersqp"); with_cache_type = Float64, with_bridge_type = Float64, ) @@ -915,7 +915,7 @@ end function test_comprehensive_duals_scalar_affine() model = MOI.instantiate( - UnoSolver.Optimizer; + () -> UnoSolver.Optimizer(preset="filtersqp"); with_cache_type = Float64, with_bridge_type = Float64, ) @@ -951,7 +951,7 @@ end function test_comprehensive_duals_scalar_nonlinear() model = MOI.instantiate( - UnoSolver.Optimizer; + () -> UnoSolver.Optimizer(preset="filtersqp"); with_cache_type = Float64, with_bridge_type = Float64, ) @@ -989,7 +989,7 @@ end function test_Parameter_basic() F, S = MOI.VariableIndex, MOI.Parameter{Float64} - model = UnoSolver.Optimizer() + model = UnoSolver.Optimizer(preset="filtersqp") @test MOI.supports_add_constrained_variable(model, S) @test !MOI.supports_constraint(model, F, S) @test isempty(MOI.get(model, MOI.ListOfConstraintTypesPresent())) diff --git a/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp b/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp index dd12c3906..e2885efa9 100644 --- a/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp +++ b/interfaces/Python/cpp_classes/UnoSolverWrapper.cpp @@ -4,9 +4,12 @@ #include #include #include +#include #include "UnoSolverWrapper.hpp" #include "model/Model.hpp" #include "options/DefaultOptions.hpp" +#include "options/Presets.hpp" +#include "options/Options.hpp" #include "tools/Logger.hpp" namespace uno { @@ -43,7 +46,7 @@ namespace uno { } UnoSolverWrapper::UnoSolverWrapper() { - DefaultOptions::load(this->options); + DefaultOptions::load(this->user_options); } void UnoSolverWrapper::set_logger_stream(py::object python_stream) { @@ -62,10 +65,20 @@ namespace uno { } Result UnoSolverWrapper::optimize(const PythonUserModel& user_model) { + // create an instance of PythonModel, a subclass of Model const PythonModel model{user_model}; - Logger::set_logger(this->options.get_string("logger")); + Logger::set_logger(this->user_options.get_string("logger")); + + // set the preset (default: auto) + Options full_options; + Presets::set(model, full_options, this->user_options.get_string("preset")); + + // copy the rest of the options + full_options.overwrite(this->user_options); + + // solve the model UnopyUserCallbacks callbacks{this->notify_acceptable_iterate_callback, this->termination_callback}; - return this->uno_solver.solve(model, this->options, callbacks); + return this->uno_solver.solve(model, full_options, callbacks); } const std::string& UnoSolverWrapper::get_method_description() const { diff --git a/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp b/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp index fa406ca4e..5ce6a7105 100644 --- a/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp +++ b/interfaces/Python/cpp_classes/UnoSolverWrapper.hpp @@ -32,7 +32,7 @@ namespace uno { class UnoSolverWrapper { public: Uno uno_solver{}; - Options options{}; + Options user_options{}; UnoSolverWrapper(); diff --git a/interfaces/Python/python_modules/UnoSolver.cpp b/interfaces/Python/python_modules/UnoSolver.cpp index d95114fce..3eac14de2 100644 --- a/interfaces/Python/python_modules/UnoSolver.cpp +++ b/interfaces/Python/python_modules/UnoSolver.cpp @@ -24,7 +24,7 @@ namespace uno { const auto type = Options::option_types.find(option_name); if (type != Options::option_types.end()) { // key found if (type->second == OptionType::BOOL) { // correct type - solver.options.set_bool(option_name, option_value); + solver.user_options.set_bool(option_name, option_value); } else { // incorrect type throw py::type_error(option_name + " is not of type bool"); @@ -39,7 +39,7 @@ namespace uno { const auto type = Options::option_types.find(option_name); if (type != Options::option_types.end()) { // key found if (type->second == OptionType::INTEGER) { // correct type - solver.options.set_integer(option_name, option_value); + solver.user_options.set_integer(option_name, option_value); } else { // incorrect type throw py::type_error(option_name + " is not of type int"); @@ -54,7 +54,7 @@ namespace uno { const auto type = Options::option_types.find(option_name); if (type != Options::option_types.end()) { // key found if (type->second == OptionType::DOUBLE) { // correct type - solver.options.set_double(option_name, option_value); + solver.user_options.set_double(option_name, option_value); } else { // incorrect type throw py::type_error(option_name + " is not of type double"); @@ -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 - solver.options.set_string(option_name, option_value); + solver.user_options.set_string(option_name, option_value); } else { // incorrect type throw py::type_error(option_name + " is not of type string"); @@ -83,7 +83,7 @@ namespace uno { .def("set_logger_stream", &UnoSolverWrapper::set_logger_stream) .def("set_preset", [](UnoSolverWrapper& solver, const std::string& preset_name) { - Presets::set(solver.options, preset_name); + Presets::set(solver.user_options, preset_name); }, py::arg("preset_name")) .def("set_notify_acceptable_iterate_callback", [](UnoSolverWrapper& solver, NotifyAcceptableIterateCallback callback) { diff --git a/uno/options/DefaultOptions.cpp b/uno/options/DefaultOptions.cpp index 85e6745e1..82b8e7fe8 100644 --- a/uno/options/DefaultOptions.cpp +++ b/uno/options/DefaultOptions.cpp @@ -11,6 +11,9 @@ namespace uno { void DefaultOptions::load(Options& options) { DefaultOptions::determine_subproblem_solvers(options); + // preset + options.set_string("preset", "auto"); + /** termination **/ // primal tolerance (constraint violation) options.set_double("primal_tolerance", 1e-8); diff --git a/uno/options/Options.cpp b/uno/options/Options.cpp index 078a989f9..bb4332e0e 100644 --- a/uno/options/Options.cpp +++ b/uno/options/Options.cpp @@ -146,6 +146,21 @@ namespace uno { } } + void Options::overwrite(const Options& options) { + for (const auto& [option_name, option_value]: options.integer_options) { + this->integer_options[option_name] = option_value; + } + for (const auto& [option_name, option_value]: options.double_options) { + this->double_options[option_name] = option_value; + } + for (const auto& [option_name, option_value]: options.bool_options) { + this->bool_options[option_name] = option_value; + } + for (const auto& [option_name, option_value]: options.string_options) { + this->string_options[option_name] = option_value; + } + } + // getters uno_int Options::get_int(const std::string& option_name) const { this->used[option_name] = true; @@ -210,7 +225,7 @@ namespace uno { OptionType Options::get_option_type(const std::string& option_name) const { try { - return this->option_types.at(option_name); + return option_types.at(option_name); } catch(const std::out_of_range&) { throw std::out_of_range("The type of the option with name " + option_name + " could not be found"); @@ -260,10 +275,6 @@ namespace uno { std::istringstream iss; iss.str(line); if (iss >> option_name >> option_value) { - // handle preset separately - if (option_name == "preset") { - Presets::set(options, option_value); - } // set option (with unknown type) options.set(option_name, option_value); } @@ -273,9 +284,9 @@ namespace uno { } void Options::dump_default_options() { + std::cout << "preset\tauto\n"; Options default_options; DefaultOptions::load(default_options); - Presets::set_default(default_options); for (const auto& [option_name, option_type]: option_types) { try { diff --git a/uno/options/Options.hpp b/uno/options/Options.hpp index 15cb3c642..d72535a7e 100644 --- a/uno/options/Options.hpp +++ b/uno/options/Options.hpp @@ -24,6 +24,8 @@ namespace uno { // setter for option with unknown type void set(const std::string& option_name, const std::string& option_value, bool flag_as_overwritten = false); + void overwrite(const Options& options); + [[nodiscard]] uno_int get_int(const std::string& option_name) const; [[nodiscard]] size_t get_unsigned_int(const std::string& option_name) const; [[nodiscard]] double get_double(const std::string& option_name) const; diff --git a/uno/options/Presets.cpp b/uno/options/Presets.cpp index d3577218f..cc210a007 100644 --- a/uno/options/Presets.cpp +++ b/uno/options/Presets.cpp @@ -1,31 +1,83 @@ -// Copyright (c) 2024 Charlie Vanaret +// Copyright (c) 2024-2026 Charlie Vanaret // Licensed under the MIT license. See LICENSE file in the project directory for details. #include #include "Presets.hpp" #include "Options.hpp" -#include "ingredients/subproblem_solvers/LPSolverFactory.hpp" -#include "ingredients/subproblem_solvers/QPSolverFactory.hpp" -#include "ingredients/subproblem_solvers/SymmetricIndefiniteLinearSolverFactory.hpp" +#include "model/Model.hpp" +#include "tools/Logger.hpp" namespace uno { - void Presets::set_default(Options& options) { - // default preset - const auto linear_solvers = SymmetricIndefiniteLinearSolverFactory::available_solvers(); - if constexpr (0 < QPSolverFactory::available_solvers.size()) { - Presets::set(options, "filtersqp"); + Preset Presets::from_string(const std::string& preset) { + if (preset == "filtersqp") { + return Preset::FILTERSQP; } - else if (!linear_solvers.empty()) { - Presets::set(options, "ipopt"); + if (preset == "ipopt") { + return Preset::IPOPT; } - else if constexpr (0 < LPSolverFactory::available_solvers.size()) { - Presets::set(options, "filterslp"); + if (preset == "funnelsqp") { + return Preset::FUNNELSQP; + } + if (preset == "filterslp") { + return Preset::FILTERSLP; + } + throw std::runtime_error("Unknown preset"); + } + + Preset Presets::pick_auto_preset(const Model& model) { + // thresholds encode "dense active-set QP stops scaling around here" + // TODO: calibrate on a representative benchmark set (CUTEst, MINLPTests) + constexpr size_t large_problem_dimension = 2000; // n + m + constexpr size_t large_problem_nonzeros = 50'000; // Jacobian + Hessian nnz + constexpr size_t many_inequalities_floor = 500; // absolute floor for the ratio rule + + const size_t total_dimension = model.number_variables + model.number_constraints; + size_t total_nonzeros = model.number_jacobian_nonzeros(); + if (model.has_hessian_matrix()) { + total_nonzeros += model.number_hessian_nonzeros(); + } + + // 1. large/sparse => barrier + if (large_problem_dimension <= total_dimension || large_problem_nonzeros <= total_nonzeros) { + INFO << "Automatically picked the ipopt preset\n"; + return Preset::IPOPT; + } + /* + // 2. many potential active constraints, and more than the DOF => barrier + if (many_inequalities_floor < number_potential_active_constraints && + model.number_variables < number_potential_active_constraints) { + INFO << "Automatically picked the ipopt preset\n"; + return Preset::IPOPT; + } + */ + // 3. small/medium with modest inequalities (and the specialized cases) => active-set SQP + else { + INFO << "Automatically picked the filtersqp preset\n"; + return Preset::FILTERSQP; + } + } + + void Presets::set(Options& options, const std::string& preset) { + if (preset == "auto") { + throw std::runtime_error("The auto preset requires the model"); + } + else { + set(options, from_string(preset)); + } + } + + void Presets::set(const Model& model, Options& options, const std::string& preset) { + if (preset == "auto") { + set(options, pick_auto_preset(model)); + } + else { + set(options, from_string(preset)); } } - void Presets::set(Options& options, const std::string& preset_name) { + void Presets::set(Options& options, Preset preset) { // shortcuts for state-of-the-art combinations - if (preset_name == "ipopt") { + if (preset == Preset::IPOPT) { options.set_string("constraint_relaxation_strategy", "feasibility_restoration"); options.set_string("inequality_handling_method", "interior_point"); options.set_string("barrier_function", "log"); @@ -57,7 +109,7 @@ namespace uno { options.set_bool("LS_scale_duals_with_step_length", true); options.set_bool("protect_actual_reduction_against_roundoff", true); } - else if (preset_name == "filtersqp") { + else if (preset == Preset::FILTERSQP) { options.set_string("constraint_relaxation_strategy", "feasibility_restoration"); options.set_string("inequality_handling_method", "inequality_constrained"); options.set_string("hessian_model", "exact"); @@ -74,7 +126,7 @@ namespace uno { options.set_bool("switch_to_optimality_requires_linearized_feasibility", true); options.set_bool("protect_actual_reduction_against_roundoff", false); } - else if (preset_name == "funnelsqp") { + else if (preset == Preset::FUNNELSQP) { options.set_string("constraint_relaxation_strategy", "feasibility_restoration"); options.set_string("inequality_handling_method", "inequality_constrained"); options.set_string("hessian_model", "exact"); @@ -99,7 +151,7 @@ namespace uno { options.set_double("funnel_switching_infeasibility_exponent", 2); options.set_integer("funnel_update_strategy", 2); } - else if (preset_name == "filterslp") { + else if (preset == Preset::FILTERSLP) { options.set_string("constraint_relaxation_strategy", "feasibility_restoration"); options.set_string("inequality_handling_method", "inequality_constrained"); options.set_string("hessian_model", "zero"); @@ -117,7 +169,7 @@ namespace uno { options.set_bool("protect_actual_reduction_against_roundoff", false); } else { - throw std::runtime_error("The preset " + preset_name + " is not known"); + throw std::runtime_error("The preset is not known"); } } } // namespace \ No newline at end of file diff --git a/uno/options/Presets.hpp b/uno/options/Presets.hpp index 7fac621ef..570290cfd 100644 --- a/uno/options/Presets.hpp +++ b/uno/options/Presets.hpp @@ -7,13 +7,26 @@ #include namespace uno { - // forward declaration + // forward declarations + class Model; class Options; + enum class Preset { + FILTERSQP = 0, + IPOPT, + FUNNELSQP, + FILTERSLP, + }; + class Presets { public: - static void set_default(Options& options); - static void set(Options& options, const std::string& preset_name); + static void set(Options& options, const std::string& preset); + static void set(const Model& model, Options& options, const std::string& preset); + + protected: + [[nodiscard]] static Preset from_string(const std::string& preset); + [[nodiscard]] static Preset pick_auto_preset(const Model& model); + static void set(Options& options, Preset preset); }; } // namespace