From 1b5010f8595bd0c3ec1f18c9bcdd4117d85143ee Mon Sep 17 00:00:00 2001 From: Abel Soares Siqueira Date: Thu, 4 Jan 2024 08:32:31 +0100 Subject: [PATCH] Create function to read parameters from file (#384) * Create function to read parameters from file * Add documentation on changing solvers and parameters * Apply suggestions from code review Co-authored-by: Lauren Clisby --------- Co-authored-by: Lauren Clisby --- .JuliaFormatter.toml | 2 +- Project.toml | 1 + docs/Project.toml | 1 + docs/src/tutorial.md | 54 ++++++++++++++++++ src/TulipaEnergyModel.jl | 2 + src/solve-model.jl | 71 ++--------------------- src/solver-parameters.jl | 118 +++++++++++++++++++++++++++++++++++++++ test/test-options.jl | 27 +++++++++ 8 files changed, 208 insertions(+), 68 deletions(-) create mode 100644 src/solver-parameters.jl diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml index 2d2fdf1d..960c4bb3 100644 --- a/.JuliaFormatter.toml +++ b/.JuliaFormatter.toml @@ -3,7 +3,7 @@ align_matrix = true align_pair_arrow = true align_struct_field = true conditional_to_if = true -format_docstrings = true +format_docstrings = false format_markdown = false indent = 4 margin = 100 diff --git a/Project.toml b/Project.toml index 70328fee..d5549fcc 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] CSV = "0.10" diff --git a/docs/Project.toml b/docs/Project.toml index 5f3d0ef4..5d33b686 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,6 @@ [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76" DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index f3c3dd42..43a7d6aa 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -110,6 +110,60 @@ solution = solve_model(model) This `solution` structure is exactly the same as the one returned when using an `EnergyProblem`. +### Change optimizer and specify parameters + +By default, the model is solved using the [HiGHS](https://github.com/jump-dev/HiGHS.jl) optimizer (or solver). +To change this, we can give the functions `run_scenario`, `solve_model`, or +`solve_model!` a different optimizer. + +For instance, we run the [Cbc](https://github.com/jump-dev/Cbc.jl) optimizer below: + +```@example +using TulipaEnergyModel, Cbc + +input_dir = "../../test/inputs/Tiny" # hide +energy_problem = run_scenario(input_dir, optimizer = Cbc.Optimizer) +``` + +or + +```@example manual-energy-problem +using Cbc + +solution = solve_model!(energy_problem, Cbc.Optimizer) +``` + +or + +```@example manual +using Cbc + +solution = solve_model(model, Cbc.Optimizer) +``` + +Notice that, in any of these cases, we need to explicitly add the Cbc package +ourselves and add `using Cbc` before using `Cbc.Optimizer`. + +In any of these cases, default parameters for the `Cbc` optimizer are used, +which you can query using [`default_parameters`](@ref). +If you want to change these, you can pass a dictionary via the keyword argument `parameters`. +For instance, in the example below, we change the maximum allowed runtime for +Cbc to be 0.01 seconds, which causes it to fail to converge in time. + +```@example +using TulipaEnergyModel, Cbc + +input_dir = "../../test/inputs/Tiny" # hide +parameters = Dict("seconds" => 0.01) +energy_problem = run_scenario(input_dir, optimizer = Cbc.Optimizer, parameters = parameters) +energy_problem.termination_status +``` + +For the full list of parameters, check your chosen optimizer. + +These parameters can also be passed via a file. See the +[`read_parameters_from_file`](@ref) function for more details. + ## [Using the graph structure](@id graph-tutorial) Read about the graph structure in the [Graph](@ref) section first. diff --git a/src/TulipaEnergyModel.jl b/src/TulipaEnergyModel.jl index 27b6e42b..1e79c2e6 100644 --- a/src/TulipaEnergyModel.jl +++ b/src/TulipaEnergyModel.jl @@ -8,11 +8,13 @@ using HiGHS using JuMP using MathOptInterface using MetaGraphsNext +using TOML include("input-tables.jl") include("structures.jl") include("io.jl") include("create-model.jl") +include("solver-parameters.jl") include("solve-model.jl") include("run-scenario.jl") include("time-resolution.jl") diff --git a/src/solve-model.jl b/src/solve-model.jl index e4bb385b..bcf664ca 100644 --- a/src/solve-model.jl +++ b/src/solve-model.jl @@ -1,69 +1,4 @@ -export solve_model!, solve_model, default_parameters - -""" - default_parameters(Val(optimizer_name_symbol)) - default_parameters(optimizer) - default_parameters(optimizer_name_symbol) - default_parameters(optimizer_name_string) - -Returns the default parameters for a given JuMP optimizer. -Falls back to `Dict()` for undefined solvers. - -## Arguments - -There are four ways to use this function: - - - `Val(optimizer_name_symbol)`: This uses type dispatch with the special `Val` type. - Just give the solver name as a Symbol (e.g., `Val(:HiGHS)`). - - `optimizer`: The JuMP optimizer type (e.g., `HiGHS.Optimizer`). - - `optimizer_name_symbol` or `optimizer_name_string`: Just give the name in Symbol - or String format and we will convert to `Val`. - -Using `Val` is necessary for the dispatch. -All other cases will convert the argument and call the `Val` version, which might lead to some type instability. - -## Examples - -```jldoctest -using HiGHS -default_parameters(HiGHS.Optimizer) - -# output - -Dict{String, Any} with 1 entry: - "output_flag" => false -``` - -This also - -```jldoctest -default_parameters(Val(:Cbc)) - -# output - -Dict{String, Any} with 1 entry: - "logLevel" => 0 -``` - -```jldoctest -default_parameters(:Cbc) == default_parameters("Cbc") == default_parameters(Val(:Cbc)) - -# output - -true -``` -""" -default_parameters(::Any) = Dict{String,Any}() -default_parameters(::Val{:HiGHS}) = Dict{String,Any}("output_flag" => false) -default_parameters(::Val{:Cbc}) = Dict{String,Any}("logLevel" => 0) -default_parameters(::Val{:GLPK}) = Dict{String,Any}("msg_lev" => 0) - -function default_parameters(::Type{T}) where {T<:MathOptInterface.AbstractOptimizer} - solver_name = split(string(T), ".")[1] - return default_parameters(Val(Symbol(solver_name))) -end - -default_parameters(optimizer::Union{String,Symbol}) = default_parameters(Val(Symbol(optimizer))) +export solve_model!, solve_model """ solution = solve_model!(energy_problem[, optimizer; parameters]) @@ -124,6 +59,8 @@ list of [supported solvers](https://jump.dev/JuMP.jl/stable/installation/#Suppor By default we use HiGHS. The keyword argument `parameters` should be passed as a list of `key => value` pairs. +These can be created manually, obtained using [`default_parameters`](@ref), or read from a file +using [`read_parameters_from_file`](@ref). The `solution` object is a NamedTuple with the following fields: @@ -160,7 +97,7 @@ The `solution` object is a NamedTuple with the following fields: ## Examples ```julia -parameters = ["presolve" => "on", "time_limit" => 60.0, "output_flag" => true] +parameters = Dict{String,Any}("presolve" => "on", "time_limit" => 60.0, "output_flag" => true) solution = solve_model(model, HiGHS.Optimizer; parameters = parameters) ``` """ diff --git a/src/solver-parameters.jl b/src/solver-parameters.jl new file mode 100644 index 00000000..dd7d6b41 --- /dev/null +++ b/src/solver-parameters.jl @@ -0,0 +1,118 @@ +export default_parameters, read_parameters_from_file + +""" + default_parameters(Val(optimizer_name_symbol)) + default_parameters(optimizer) + default_parameters(optimizer_name_symbol) + default_parameters(optimizer_name_string) + +Returns the default parameters for a given JuMP optimizer. +Falls back to `Dict()` for undefined solvers. + +## Arguments + +There are four ways to use this function: + + - `Val(optimizer_name_symbol)`: This uses type dispatch with the special `Val` type. + Just give the solver name as a Symbol (e.g., `Val(:HiGHS)`). + - `optimizer`: The JuMP optimizer type (e.g., `HiGHS.Optimizer`). + - `optimizer_name_symbol` or `optimizer_name_string`: Just give the name in Symbol + or String format and we will convert to `Val`. + +Using `Val` is necessary for the dispatch. +All other cases will convert the argument and call the `Val` version, which might lead to some type instability. + +## Examples + +```jldoctest +using HiGHS +default_parameters(HiGHS.Optimizer) + +# output + +Dict{String, Any} with 1 entry: + "output_flag" => false +``` + +Another case + +```jldoctest +default_parameters(Val(:Cbc)) + +# output + +Dict{String, Any} with 1 entry: + "logLevel" => 0 +``` + +```jldoctest +default_parameters(:Cbc) == default_parameters("Cbc") == default_parameters(Val(:Cbc)) + +# output + +true +``` +""" +default_parameters(::Any) = Dict{String,Any}() +default_parameters(::Val{:HiGHS}) = Dict{String,Any}("output_flag" => false) +default_parameters(::Val{:Cbc}) = Dict{String,Any}("logLevel" => 0) +default_parameters(::Val{:GLPK}) = Dict{String,Any}("msg_lev" => 0) + +function default_parameters(::Type{T}) where {T<:MathOptInterface.AbstractOptimizer} + solver_name = split(string(T), ".")[1] + return default_parameters(Val(Symbol(solver_name))) +end + +default_parameters(optimizer::Union{String,Symbol}) = default_parameters(Val(Symbol(optimizer))) + +""" + read_parameters_from_file(filepath) + +Parse the parameters from a file into a dictionary. +The keys and values are NOT checked to be valid parameters for any specific solvers. + +The file should contain a list of lines of the following type: + +```toml +key = value +``` + +The file is parsed as [TOML](https://toml.io), which is very intuitive. See the example below. + +## Example + +```jldoctest +# Creating file +filepath, io = mktemp() +println(io, + \"\"\" + true_or_false = true + integer_number = 5 + real_number1 = 3.14 + big_number = 6.66E06 + small_number = 1e-8 + string = "something" + \"\"\" +) +close(io) +# Reading +read_parameters_from_file(filepath) + +# output + +Dict{String, Any} with 6 entries: + "string" => "something" + "integer_number" => 5 + "small_number" => 1.0e-8 + "true_or_false" => true + "real_number1" => 3.14 + "big_number" => 6.66e6 +``` +""" +function read_parameters_from_file(filepath) + if !isfile(filepath) + throw(ArgumentError("'$filepath' is not a file.")) + end + + return TOML.parsefile(filepath) +end diff --git a/test/test-options.jl b/test/test-options.jl index ee5b4156..da9126be 100644 --- a/test/test-options.jl +++ b/test/test-options.jl @@ -43,6 +43,33 @@ end end end +@testset "Test reading from file" begin + filepath, io = mktemp() + println( + io, + """ + true_or_false = true + integer_number = 5 + real_number1 = 3.14 + big_number = 6.66E06 + small_number = 1e-8 + string = "something" + """, + ) + close(io) + + @test read_parameters_from_file(filepath) == Dict{String,Any}( + "string" => "something", + "integer_number" => 5, + "small_number" => 1.0e-8, + "true_or_false" => true, + "real_number1" => 3.14, + "big_number" => 6.66e6, + ) + + @test_throws ArgumentError read_parameters_from_file("badfile") +end + @testset "Test that bad options throw errors" begin dir = joinpath(INPUT_FOLDER, "Tiny") @test_throws MathOptInterface.UnsupportedAttribute energy_problem = run_scenario(