From c5585a196e267a9828393d915d913ad147e055c3 Mon Sep 17 00:00:00 2001 From: Zengjian Hu Date: Thu, 21 Sep 2023 17:26:55 -0700 Subject: [PATCH 01/13] implement solve_all --- src/optimize.jl | 75 ++++++++++++++------ test/examples/nqueens_solveall.jl | 114 ++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 22 deletions(-) create mode 100644 test/examples/nqueens_solveall.jl diff --git a/src/optimize.jl b/src/optimize.jl index 16f827e..b0f564f 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -25,23 +25,25 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer inner::Model{T} solver_status::String primal_objective::T - primal_solution::Dict{MOI.VariableIndex,T} + primal_solutions::Vector{Dict{MOI.VariableIndex, T}} # all solutions options::Dict{String,Any} + n_sols::Int # Max number of solutions time_limit_sec::Union{Nothing,Float64} solve_time_sec::Float64 function Optimizer{T}(solver::String) where {T} if solver == "chuffed" solver = Chuffed() end - primal_solution = Dict{MOI.VariableIndex,T}() - options = Dict{String,Any}("model_filename" => "") + primal_solutions = Vector{Dict{MOI.VariableIndex,T}}() + options = Dict{String,Any}("model_filename" => "", "all_solutions" => false) return new( solver, Model{T}(), "", zero(T), - primal_solution, + primal_solutions, options, + 1, nothing, NaN, ) @@ -77,13 +79,23 @@ function _run_minizinc(dest::Optimizer) end output = joinpath(dir, "model.ozn") _stdout = joinpath(dir, "_stdout.txt") + + dest.n_sols = if dest.options["all_solutions"] + get(dest.options, "num_solutions", (Int)(typemax(Int32))) + else + 1 # only one solution if all_solutions is false + end + _minizinc_exe() do exe - cmd = if dest.time_limit_sec !== nothing + cmd = `$(exe) --solver $(dest.solver) --output-objective -o $(output) $(filename)` + + if dest.time_limit_sec !== nothing limit = round(Int, 1_000 * dest.time_limit_sec::Float64) - `$(exe) --solver $(dest.solver) --output-objective --time-limit $limit -o $(output) $(filename)` - else - `$(exe) --solver $(dest.solver) --output-objective -o $(output) $(filename)` + cmd = `$cmd --time-limit $limit` end + if dest.n_sols > 1 # only add --num-solutions if n_sols > 1 + cmd = `$cmd --num-solutions $(dest.n_sols)` + end return run(pipeline(cmd, stdout = _stdout)) end if isfile(output) @@ -115,7 +127,7 @@ function MOI.empty!(model::Optimizer{T}) where {T} empty!(model.inner.ext) model.solver_status = "" model.primal_objective = zero(T) - empty!(model.primal_solution) + empty!(model.primal_solutions) model.solve_time_sec = NaN return end @@ -172,8 +184,10 @@ end function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} time_start = time() MOI.empty!(dest.inner) - empty!(dest.primal_solution) + empty!(dest.primal_solutions) index_map = MOI.copy_to(dest.inner, src) + + ret = _run_minizinc(dest) if !isempty(ret) m_stat = match(r"=====(.+)=====", ret) @@ -186,15 +200,19 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} MOI.get(dest.inner, MOI.VariableName(), x) => x for x in MOI.get(src, MOI.ListOfVariableIndices()) ) + + primal_solution = Dict{MOI.VariableIndex, T}() for line in split(ret, "\n") m_var = match(r"(.+) \= (.+)\;", line) if m_var === nothing + isempty(primal_solution) || push!(dest.primal_solutions, primal_solution) # add solution + primal_solution = Dict{MOI.VariableIndex, T}() continue elseif m_var[1] == "_objective" dest.primal_objective = _parse_result(T, m_var[2]) else x = variable_map[m_var[1]] - dest.primal_solution[x] = _parse_result(T, m_var[2]) + primal_solution[x] = _parse_result(T, m_var[2]) end end end @@ -209,7 +227,7 @@ end function _has_solution(model::Optimizer) return model.solver_status == "SATISFIABLE" && - !isempty(model.primal_solution) + !isempty(model.primal_solutions) end MOI.get(model::Optimizer, ::MOI.RawStatusString) = model.solver_status @@ -218,7 +236,11 @@ function MOI.get(model::Optimizer, ::MOI.TerminationStatus) if model.solver_status == "UNSATISFIABLE" return MOI.INFEASIBLE elseif _has_solution(model) - return MOI.OPTIMAL + if model.n_sols > 1 && length(model.primal_solutions) >= model.n_sols + return MOI.SOLUTION_LIMIT + else + return MOI.OPTIMAL + end else return MOI.OTHER_ERROR end @@ -234,23 +256,32 @@ end MOI.get(::Optimizer, ::MOI.DualStatus) = MOI.NO_SOLUTION -function MOI.get(model::Optimizer, ::MOI.ResultCount) - return _has_solution(model) ? 1 : 0 -end +MOI.get(model::Optimizer, ::MOI.ResultCount) = length(model.primal_solutions) function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) MOI.check_result_index_bounds(model, attr) return model.primal_objective end -function MOI.get( - model::Optimizer, - attr::MOI.VariablePrimal, - x::MOI.VariableIndex, -) +function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, x::MOI.VariableIndex) MOI.check_result_index_bounds(model, attr) MOI.throw_if_not_valid(model, x) - return model.primal_solution[x] + if attr.result_index > length(model.primal_solutions) + throw(ErrorException("Result index out of bounds.")) + end + return model.primal_solutions[attr.result_index][x] end MOI.get(model::Optimizer, ::MOI.SolveTimeSec) = model.solve_time_sec + +function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) + if attr.name == "all_solutions" + value === true || + value === false || + throw(ErrorException("Invalid value $value for $(attr.name).")) + elseif attr.name == "num_solutions" + (value isa Int && value >= 1) || + throw(ErrorException("Invalid value $value for $(attr.name).")) + end + model.options[attr.name] = value +end \ No newline at end of file diff --git a/test/examples/nqueens_solveall.jl b/test/examples/nqueens_solveall.jl new file mode 100644 index 0000000..e1a83c6 --- /dev/null +++ b/test/examples/nqueens_solveall.jl @@ -0,0 +1,114 @@ +# Copyright (c) 2022 MiniZinc.jl 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. + +# N-queens example +# based on MiniZinc example nqueens.mzn +# queen in column i is in row q[i] + +n = 8 + +function init_model() + model = MOI.instantiate( + () -> MiniZinc.Optimizer{Int}("chuffed"); + with_cache_type = Int, + with_bridge_type = Int, + ) + MOI.set(model, MOI.RawOptimizerAttribute("model_filename"), "test.mzn") + q = MOI.add_variables(model, n) + MOI.add_constraint.(model, q, MOI.Interval(1, n)) + MOI.add_constraint(model, MOI.VectorOfVariables(q), MOI.AllDifferent(n)) + for op in (+, -) + f = MOI.Utilities.vectorize([op(q[i], i) for i in eachindex(q)]) + MOI.add_constraint(model, f, MOI.AllDifferent(n)) + end + + return model, q +end + +function check_result( + model, + q, + actual_count = 92, + termination_status = MOI.OPTIMAL, +) + @test MOI.get(model, MOI.TerminationStatus()) === termination_status + res_count = MOI.get(model, MOI.ResultCount()) + @test res_count == actual_count + + for i in 1:res_count + q_sol = MOI.get(model, MOI.VariablePrimal(i), q) + @test allunique(q_sol) + @test allunique(q_sol .+ (1:n)) + @test allunique(q_sol .- (1:n)) + end + @test MOI.get(model, MOI.SolveTimeSec()) < 4.0 + return rm("test.mzn") +end + +function run_nqueens(option) + if option == "solve_all1" # solve all + @info "test solve_all with num_solution not set" + model, q = init_model() + MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), true) + MOI.optimize!(model) + check_result(model, q) + elseif option == "solve_all2" # solve all with limit > 92 + @info "test solve_all with limit > 92" + model, q = init_model() + MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), true) + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) + MOI.optimize!(model) + check_result(model, q) + elseif option == "solve_all3" # solve all with limit = 25 + @info "test solve_all with limit = 25" + model, q = init_model() + MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), true) + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 25) + MOI.optimize!(model) + check_result(model, q, 25, MOI.SOLUTION_LIMIT) + elseif option == "solve_one" # solve one + @info "test solve_one" + model, q = init_model() + MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), false) + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) + MOI.optimize!(model) + check_result(model, q, 1) + elseif option == "throw" + @info "test throw" + model, q = init_model() + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("all_solutions"), + 1, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + -1, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + 0, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + 1.1, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + "two", + ) + end + return +end + +test_nqueens1() = run_nqueens("solve_all1") +test_nqueens2() = run_nqueens("solve_all2") +test_nqueens3() = run_nqueens("solve_all3") +test_nqueens4() = run_nqueens("solve_one") +test_nqueens5() = run_nqueens("throw") From d3c69e95699c6af051c93f85d062354646b515ee Mon Sep 17 00:00:00 2001 From: Zengjian Hu Date: Thu, 21 Sep 2023 17:32:13 -0700 Subject: [PATCH 02/13] format --- src/optimize.jl | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/optimize.jl b/src/optimize.jl index b0f564f..25bac9f 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -25,9 +25,9 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer inner::Model{T} solver_status::String primal_objective::T - primal_solutions::Vector{Dict{MOI.VariableIndex, T}} # all solutions + primal_solutions::Vector{Dict{MOI.VariableIndex,T}} # all solutions options::Dict{String,Any} - n_sols::Int # Max number of solutions + ub_sols::Int # Max number of solutions time_limit_sec::Union{Nothing,Float64} solve_time_sec::Float64 function Optimizer{T}(solver::String) where {T} @@ -35,7 +35,8 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer solver = Chuffed() end primal_solutions = Vector{Dict{MOI.VariableIndex,T}}() - options = Dict{String,Any}("model_filename" => "", "all_solutions" => false) + options = + Dict{String,Any}("model_filename" => "", "all_solutions" => false) return new( solver, Model{T}(), @@ -80,7 +81,7 @@ function _run_minizinc(dest::Optimizer) output = joinpath(dir, "model.ozn") _stdout = joinpath(dir, "_stdout.txt") - dest.n_sols = if dest.options["all_solutions"] + dest.ub_sols = if dest.options["all_solutions"] get(dest.options, "num_solutions", (Int)(typemax(Int32))) else 1 # only one solution if all_solutions is false @@ -89,13 +90,13 @@ function _run_minizinc(dest::Optimizer) _minizinc_exe() do exe cmd = `$(exe) --solver $(dest.solver) --output-objective -o $(output) $(filename)` - if dest.time_limit_sec !== nothing + if dest.time_limit_sec !== nothing limit = round(Int, 1_000 * dest.time_limit_sec::Float64) - cmd = `$cmd --time-limit $limit` + cmd = `$cmd --time-limit $limit` + end + if dest.ub_sols > 1 # only add --num-solutions if ub_sols > 1 + cmd = `$cmd --num-solutions $(dest.ub_sols)` end - if dest.n_sols > 1 # only add --num-solutions if n_sols > 1 - cmd = `$cmd --num-solutions $(dest.n_sols)` - end return run(pipeline(cmd, stdout = _stdout)) end if isfile(output) @@ -187,7 +188,6 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} empty!(dest.primal_solutions) index_map = MOI.copy_to(dest.inner, src) - ret = _run_minizinc(dest) if !isempty(ret) m_stat = match(r"=====(.+)=====", ret) @@ -201,12 +201,13 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} x in MOI.get(src, MOI.ListOfVariableIndices()) ) - primal_solution = Dict{MOI.VariableIndex, T}() + primal_solution = Dict{MOI.VariableIndex,T}() for line in split(ret, "\n") m_var = match(r"(.+) \= (.+)\;", line) if m_var === nothing - isempty(primal_solution) || push!(dest.primal_solutions, primal_solution) # add solution - primal_solution = Dict{MOI.VariableIndex, T}() + isempty(primal_solution) || + push!(dest.primal_solutions, primal_solution) # add solution + primal_solution = Dict{MOI.VariableIndex,T}() continue elseif m_var[1] == "_objective" dest.primal_objective = _parse_result(T, m_var[2]) @@ -236,9 +237,9 @@ function MOI.get(model::Optimizer, ::MOI.TerminationStatus) if model.solver_status == "UNSATISFIABLE" return MOI.INFEASIBLE elseif _has_solution(model) - if model.n_sols > 1 && length(model.primal_solutions) >= model.n_sols + if model.ub_sols > 1 && length(model.primal_solutions) >= model.ub_sols return MOI.SOLUTION_LIMIT - else + else return MOI.OPTIMAL end else @@ -263,7 +264,11 @@ function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue) return model.primal_objective end -function MOI.get(model::Optimizer, attr::MOI.VariablePrimal, x::MOI.VariableIndex) +function MOI.get( + model::Optimizer, + attr::MOI.VariablePrimal, + x::MOI.VariableIndex, +) MOI.check_result_index_bounds(model, attr) MOI.throw_if_not_valid(model, x) if attr.result_index > length(model.primal_solutions) @@ -283,5 +288,5 @@ function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) (value isa Int && value >= 1) || throw(ErrorException("Invalid value $value for $(attr.name).")) end - model.options[attr.name] = value -end \ No newline at end of file + return model.options[attr.name] = value +end From 32902580799b8461fd1cf93321f62495da2eeff7 Mon Sep 17 00:00:00 2001 From: Zengjian Hu Date: Thu, 21 Sep 2023 19:44:09 -0700 Subject: [PATCH 03/13] address Oscar's comments --- src/optimize.jl | 36 +++------ test/examples/nqueens_solveall.jl | 126 ++++++++++++++---------------- 2 files changed, 70 insertions(+), 92 deletions(-) diff --git a/src/optimize.jl b/src/optimize.jl index 25bac9f..e8b554c 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -27,16 +27,14 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer primal_objective::T primal_solutions::Vector{Dict{MOI.VariableIndex,T}} # all solutions options::Dict{String,Any} - ub_sols::Int # Max number of solutions time_limit_sec::Union{Nothing,Float64} solve_time_sec::Float64 function Optimizer{T}(solver::String) where {T} if solver == "chuffed" solver = Chuffed() end - primal_solutions = Vector{Dict{MOI.VariableIndex,T}}() - options = - Dict{String,Any}("model_filename" => "", "all_solutions" => false) + primal_solutions = Dict{MOI.VariableIndex,T}[] + options = Dict{String,Any}("model_filename" => "", "num_solutions" => 1) return new( solver, Model{T}(), @@ -44,7 +42,6 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer zero(T), primal_solutions, options, - 1, nothing, NaN, ) @@ -81,12 +78,6 @@ function _run_minizinc(dest::Optimizer) output = joinpath(dir, "model.ozn") _stdout = joinpath(dir, "_stdout.txt") - dest.ub_sols = if dest.options["all_solutions"] - get(dest.options, "num_solutions", (Int)(typemax(Int32))) - else - 1 # only one solution if all_solutions is false - end - _minizinc_exe() do exe cmd = `$(exe) --solver $(dest.solver) --output-objective -o $(output) $(filename)` @@ -94,8 +85,9 @@ function _run_minizinc(dest::Optimizer) limit = round(Int, 1_000 * dest.time_limit_sec::Float64) cmd = `$cmd --time-limit $limit` end - if dest.ub_sols > 1 # only add --num-solutions if ub_sols > 1 - cmd = `$cmd --num-solutions $(dest.ub_sols)` + + if dest.options["num_solutions"] > 1 + cmd = `$cmd --num-solutions $(dest.options["num_solutions"])` end return run(pipeline(cmd, stdout = _stdout)) end @@ -141,11 +133,6 @@ function MOI.get(model::Optimizer, attr::MOI.RawOptimizerAttribute) return get(model.options, attr.name, nothing) end -function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) - model.options[attr.name] = value - return -end - MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true MOI.get(model::Optimizer, ::MOI.TimeLimitSec) = model.time_limit_sec @@ -237,7 +224,8 @@ function MOI.get(model::Optimizer, ::MOI.TerminationStatus) if model.solver_status == "UNSATISFIABLE" return MOI.INFEASIBLE elseif _has_solution(model) - if model.ub_sols > 1 && length(model.primal_solutions) >= model.ub_sols + if model.options["num_solutions"] > 1 && + length(model.primal_solutions) >= model.options["num_solutions"] return MOI.SOLUTION_LIMIT else return MOI.OPTIMAL @@ -280,13 +268,11 @@ end MOI.get(model::Optimizer, ::MOI.SolveTimeSec) = model.solve_time_sec function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) - if attr.name == "all_solutions" - value === true || - value === false || - throw(ErrorException("Invalid value $value for $(attr.name).")) - elseif attr.name == "num_solutions" + if attr.name == "num_solutions" (value isa Int && value >= 1) || throw(ErrorException("Invalid value $value for $(attr.name).")) end - return model.options[attr.name] = value + + model.options[attr.name] = value + return end diff --git a/test/examples/nqueens_solveall.jl b/test/examples/nqueens_solveall.jl index e1a83c6..013b301 100644 --- a/test/examples/nqueens_solveall.jl +++ b/test/examples/nqueens_solveall.jl @@ -3,13 +3,13 @@ # 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. -# N-queens example +# N-queens example - solve_all # based on MiniZinc example nqueens.mzn # queen in column i is in row q[i] n = 8 -function init_model() +function _init_model() model = MOI.instantiate( () -> MiniZinc.Optimizer{Int}("chuffed"); with_cache_type = Int, @@ -27,7 +27,7 @@ function init_model() return model, q end -function check_result( +function _check_result( model, q, actual_count = 92, @@ -43,72 +43,64 @@ function check_result( @test allunique(q_sol .+ (1:n)) @test allunique(q_sol .- (1:n)) end + @test MOI.get(model, MOI.SolveTimeSec()) < 4.0 - return rm("test.mzn") + rm("test.mzn") + return end -function run_nqueens(option) - if option == "solve_all1" # solve all - @info "test solve_all with num_solution not set" - model, q = init_model() - MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), true) - MOI.optimize!(model) - check_result(model, q) - elseif option == "solve_all2" # solve all with limit > 92 - @info "test solve_all with limit > 92" - model, q = init_model() - MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), true) - MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) - MOI.optimize!(model) - check_result(model, q) - elseif option == "solve_all3" # solve all with limit = 25 - @info "test solve_all with limit = 25" - model, q = init_model() - MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), true) - MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 25) - MOI.optimize!(model) - check_result(model, q, 25, MOI.SOLUTION_LIMIT) - elseif option == "solve_one" # solve one - @info "test solve_one" - model, q = init_model() - MOI.set(model, MOI.RawOptimizerAttribute("all_solutions"), false) - MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) - MOI.optimize!(model) - check_result(model, q, 1) - elseif option == "throw" - @info "test throw" - model, q = init_model() - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("all_solutions"), - 1, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - -1, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - 0, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - 1.1, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - "two", - ) - end - return +function test_solve_all1() # solve all with limit > 92 + @info "test solve_all with limit > 92" + model, q = _init_model() + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) + MOI.optimize!(model) + return _check_result(model, q) +end + +function test_solve_all2() # solve all with limit = 25 + @info "test solve_all with limit = 25" + model, q = _init_model() + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 25) + MOI.optimize!(model) + return _check_result(model, q, 25, MOI.SOLUTION_LIMIT) +end + +function test_solve_one1() # solve one with limit not set + @info "test solve_one with limit not set" + model, q = _init_model() + MOI.optimize!(model) + return _check_result(model, q, 1) end -test_nqueens1() = run_nqueens("solve_all1") -test_nqueens2() = run_nqueens("solve_all2") -test_nqueens3() = run_nqueens("solve_all3") -test_nqueens4() = run_nqueens("solve_one") -test_nqueens5() = run_nqueens("throw") +function test_solve_one2() # solve one with limit = 1 + @info "test solve_one with limit = 1" + model, q = _init_model() + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 1) + MOI.optimize!(model) + return _check_result(model, q, 1) +end + +function test_throw() # test throw + @info "test throw" + model, _ = _init_model() + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + -1, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + 0, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + 1.1, + ) + @test_throws ErrorException MOI.set( + model, + MOI.RawOptimizerAttribute("num_solutions"), + "two", + ) +end From 608540bbc2458f578981c37094e7fb194b885e99 Mon Sep 17 00:00:00 2001 From: zengjian-hu-rai <106710107+zengjian-hu-rai@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:50:43 -0700 Subject: [PATCH 04/13] Apply suggestions from code review Co-authored-by: Oscar Dowson --- src/optimize.jl | 12 ++++++++---- test/examples/nqueens_solveall.jl | 2 -- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/optimize.jl b/src/optimize.jl index e8b554c..00798bd 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -25,7 +25,7 @@ mutable struct Optimizer{T} <: MOI.AbstractOptimizer inner::Model{T} solver_status::String primal_objective::T - primal_solutions::Vector{Dict{MOI.VariableIndex,T}} # all solutions + primal_solutions::Vector{Dict{MOI.VariableIndex,T}} options::Dict{String,Any} time_limit_sec::Union{Nothing,Float64} solve_time_sec::Float64 @@ -174,7 +174,6 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} MOI.empty!(dest.inner) empty!(dest.primal_solutions) index_map = MOI.copy_to(dest.inner, src) - ret = _run_minizinc(dest) if !isempty(ret) m_stat = match(r"=====(.+)=====", ret) @@ -187,14 +186,19 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} MOI.get(dest.inner, MOI.VariableName(), x) => x for x in MOI.get(src, MOI.ListOfVariableIndices()) ) - primal_solution = Dict{MOI.VariableIndex,T}() for line in split(ret, "\n") m_var = match(r"(.+) \= (.+)\;", line) if m_var === nothing isempty(primal_solution) || push!(dest.primal_solutions, primal_solution) # add solution - primal_solution = Dict{MOI.VariableIndex,T}() + if !isempty(primal_solution) + # We found a line in the output that is not a variable + # statement. It must divide the solutions, so append + # the current. + push!(dest.primal_solutions, copy(primal_solution)) + empty!(primal_solution) + end continue elseif m_var[1] == "_objective" dest.primal_objective = _parse_result(T, m_var[2]) diff --git a/test/examples/nqueens_solveall.jl b/test/examples/nqueens_solveall.jl index 013b301..87717b4 100644 --- a/test/examples/nqueens_solveall.jl +++ b/test/examples/nqueens_solveall.jl @@ -23,7 +23,6 @@ function _init_model() f = MOI.Utilities.vectorize([op(q[i], i) for i in eachindex(q)]) MOI.add_constraint(model, f, MOI.AllDifferent(n)) end - return model, q end @@ -36,7 +35,6 @@ function _check_result( @test MOI.get(model, MOI.TerminationStatus()) === termination_status res_count = MOI.get(model, MOI.ResultCount()) @test res_count == actual_count - for i in 1:res_count q_sol = MOI.get(model, MOI.VariablePrimal(i), q) @test allunique(q_sol) From 7452f07c5504f7a8bfcdf0fa384620a9b8f717fa Mon Sep 17 00:00:00 2001 From: Zengjian Hu Date: Thu, 21 Sep 2023 19:58:37 -0700 Subject: [PATCH 05/13] address Oscar's comments - 2 --- src/optimize.jl | 5 ----- test/examples/nqueens_solveall.jl | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/optimize.jl b/src/optimize.jl index 00798bd..0e8ccfe 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -190,8 +190,6 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} for line in split(ret, "\n") m_var = match(r"(.+) \= (.+)\;", line) if m_var === nothing - isempty(primal_solution) || - push!(dest.primal_solutions, primal_solution) # add solution if !isempty(primal_solution) # We found a line in the output that is not a variable # statement. It must divide the solutions, so append @@ -263,9 +261,6 @@ function MOI.get( ) MOI.check_result_index_bounds(model, attr) MOI.throw_if_not_valid(model, x) - if attr.result_index > length(model.primal_solutions) - throw(ErrorException("Result index out of bounds.")) - end return model.primal_solutions[attr.result_index][x] end diff --git a/test/examples/nqueens_solveall.jl b/test/examples/nqueens_solveall.jl index 87717b4..5981243 100644 --- a/test/examples/nqueens_solveall.jl +++ b/test/examples/nqueens_solveall.jl @@ -7,9 +7,8 @@ # based on MiniZinc example nqueens.mzn # queen in column i is in row q[i] -n = 8 - function _init_model() + n = 8 model = MOI.instantiate( () -> MiniZinc.Optimizer{Int}("chuffed"); with_cache_type = Int, @@ -32,6 +31,7 @@ function _check_result( actual_count = 92, termination_status = MOI.OPTIMAL, ) + n = 8 @test MOI.get(model, MOI.TerminationStatus()) === termination_status res_count = MOI.get(model, MOI.ResultCount()) @test res_count == actual_count From 9708d3d9202be1729e0772210712511fdf22617c Mon Sep 17 00:00:00 2001 From: Zengjian Hu Date: Thu, 21 Sep 2023 20:05:37 -0700 Subject: [PATCH 06/13] minor function name change --- test/examples/nqueens_solveall.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/examples/nqueens_solveall.jl b/test/examples/nqueens_solveall.jl index 5981243..b354185 100644 --- a/test/examples/nqueens_solveall.jl +++ b/test/examples/nqueens_solveall.jl @@ -47,7 +47,7 @@ function _check_result( return end -function test_solve_all1() # solve all with limit > 92 +function test_nqueens_solve_all1() # solve all with limit > 92 @info "test solve_all with limit > 92" model, q = _init_model() MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) @@ -55,7 +55,7 @@ function test_solve_all1() # solve all with limit > 92 return _check_result(model, q) end -function test_solve_all2() # solve all with limit = 25 +function test_nqueens_solve_all2() # solve all with limit = 25 @info "test solve_all with limit = 25" model, q = _init_model() MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 25) @@ -63,14 +63,14 @@ function test_solve_all2() # solve all with limit = 25 return _check_result(model, q, 25, MOI.SOLUTION_LIMIT) end -function test_solve_one1() # solve one with limit not set +function test_nqueens_solve_one1() # solve one with limit not set @info "test solve_one with limit not set" model, q = _init_model() MOI.optimize!(model) return _check_result(model, q, 1) end -function test_solve_one2() # solve one with limit = 1 +function test_nqueens_solve_one2() # solve one with limit = 1 @info "test solve_one with limit = 1" model, q = _init_model() MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 1) @@ -78,7 +78,7 @@ function test_solve_one2() # solve one with limit = 1 return _check_result(model, q, 1) end -function test_throw() # test throw +function test_nqueens_throw() # test throw @info "test throw" model, _ = _init_model() @test_throws ErrorException MOI.set( From d4a3986e2f35c3996310e3fdf201ee14956a5489 Mon Sep 17 00:00:00 2001 From: zengjian-hu-rai <106710107+zengjian-hu-rai@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:25:12 -0700 Subject: [PATCH 07/13] Update src/optimize.jl Co-authored-by: Oscar Dowson --- src/optimize.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/optimize.jl b/src/optimize.jl index 0e8ccfe..a86fa62 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -77,7 +77,6 @@ function _run_minizinc(dest::Optimizer) end output = joinpath(dir, "model.ozn") _stdout = joinpath(dir, "_stdout.txt") - _minizinc_exe() do exe cmd = `$(exe) --solver $(dest.solver) --output-objective -o $(output) $(filename)` From bc717dd624d3a327b01e2cbe78f20340d22beca2 Mon Sep 17 00:00:00 2001 From: zengjian-hu-rai <106710107+zengjian-hu-rai@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:25:20 -0700 Subject: [PATCH 08/13] Update src/optimize.jl Co-authored-by: Oscar Dowson --- src/optimize.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/optimize.jl b/src/optimize.jl index a86fa62..9b4d593 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -84,7 +84,6 @@ function _run_minizinc(dest::Optimizer) limit = round(Int, 1_000 * dest.time_limit_sec::Float64) cmd = `$cmd --time-limit $limit` end - if dest.options["num_solutions"] > 1 cmd = `$cmd --num-solutions $(dest.options["num_solutions"])` end From 2990f3f727880f71c37bcd24436c3b85492eb4b1 Mon Sep 17 00:00:00 2001 From: zengjian-hu-rai <106710107+zengjian-hu-rai@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:25:29 -0700 Subject: [PATCH 09/13] Update src/optimize.jl Co-authored-by: Oscar Dowson --- src/optimize.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/optimize.jl b/src/optimize.jl index 9b4d593..1c2f1b0 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -79,7 +79,6 @@ function _run_minizinc(dest::Optimizer) _stdout = joinpath(dir, "_stdout.txt") _minizinc_exe() do exe cmd = `$(exe) --solver $(dest.solver) --output-objective -o $(output) $(filename)` - if dest.time_limit_sec !== nothing limit = round(Int, 1_000 * dest.time_limit_sec::Float64) cmd = `$cmd --time-limit $limit` From 98a30ee1e0ca4239791833bc1670fc0c8eb319a3 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 22 Sep 2023 15:30:18 +1200 Subject: [PATCH 10/13] Update optimize.jl --- src/optimize.jl | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/optimize.jl b/src/optimize.jl index 1c2f1b0..853adee 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -130,6 +130,14 @@ function MOI.get(model::Optimizer, attr::MOI.RawOptimizerAttribute) return get(model.options, attr.name, nothing) end +function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) + if attr.name == "num_solutions" && !(value isa Int && value >= 1) + throw(MOI.SetAttributeNotAllowed("value must be an `Int` that is >= 1")) + end + model.options[attr.name] = value + return +end + MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true MOI.get(model::Optimizer, ::MOI.TimeLimitSec) = model.time_limit_sec @@ -223,8 +231,7 @@ function MOI.get(model::Optimizer, ::MOI.TerminationStatus) if model.solver_status == "UNSATISFIABLE" return MOI.INFEASIBLE elseif _has_solution(model) - if model.options["num_solutions"] > 1 && - length(model.primal_solutions) >= model.options["num_solutions"] + if 1 < model.options["num_solutions"] <= length(model.primal_solutions) return MOI.SOLUTION_LIMIT else return MOI.OPTIMAL @@ -262,13 +269,3 @@ function MOI.get( end MOI.get(model::Optimizer, ::MOI.SolveTimeSec) = model.solve_time_sec - -function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) - if attr.name == "num_solutions" - (value isa Int && value >= 1) || - throw(ErrorException("Invalid value $value for $(attr.name).")) - end - - model.options[attr.name] = value - return -end From 95793b42cb491a6c97682994aba711516ac679fb Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 22 Sep 2023 15:41:00 +1200 Subject: [PATCH 11/13] Update nqueens_solveall.jl --- test/examples/nqueens_solveall.jl | 78 +++++++++++-------------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/test/examples/nqueens_solveall.jl b/test/examples/nqueens_solveall.jl index b354185..8105656 100644 --- a/test/examples/nqueens_solveall.jl +++ b/test/examples/nqueens_solveall.jl @@ -3,11 +3,7 @@ # 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. -# N-queens example - solve_all -# based on MiniZinc example nqueens.mzn -# queen in column i is in row q[i] - -function _init_model() +function _init_nqueens_solve_num_solutions() n = 8 model = MOI.instantiate( () -> MiniZinc.Optimizer{Int}("chuffed"); @@ -25,13 +21,14 @@ function _init_model() return model, q end -function _check_result( +function _test_nqueens_solve_num_solutions( model, q, actual_count = 92, termination_status = MOI.OPTIMAL, ) n = 8 + MOI.optimize!(model) @test MOI.get(model, MOI.TerminationStatus()) === termination_status res_count = MOI.get(model, MOI.ResultCount()) @test res_count == actual_count @@ -41,64 +38,45 @@ function _check_result( @test allunique(q_sol .+ (1:n)) @test allunique(q_sol .- (1:n)) end - @test MOI.get(model, MOI.SolveTimeSec()) < 4.0 rm("test.mzn") return end -function test_nqueens_solve_all1() # solve all with limit > 92 - @info "test solve_all with limit > 92" - model, q = _init_model() +function test_nqueens_solve_num_solutions_100() + model, q = _init_nqueens_solve_num_solutions() MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 100) - MOI.optimize!(model) - return _check_result(model, q) + _test_nqueens_solve_num_solutions(model, q) + return end -function test_nqueens_solve_all2() # solve all with limit = 25 - @info "test solve_all with limit = 25" - model, q = _init_model() +function test_nqueens_solve_num_solutions_25() + model, q = _init_nqueens_solve_num_solutions() MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 25) - MOI.optimize!(model) - return _check_result(model, q, 25, MOI.SOLUTION_LIMIT) + _test_nqueens_solve_num_solutions(model, q, 25, MOI.SOLUTION_LIMIT) + return end -function test_nqueens_solve_one1() # solve one with limit not set - @info "test solve_one with limit not set" - model, q = _init_model() - MOI.optimize!(model) - return _check_result(model, q, 1) +function test_nqueens_solve_num_solutions_not_set() + model, q = _init_nqueens_solve_num_solutions() + _test_nqueens_solve_num_solutions(model, q, 1) + return end -function test_nqueens_solve_one2() # solve one with limit = 1 - @info "test solve_one with limit = 1" - model, q = _init_model() +function test_nqueens_solve_num_solutions_1() + model, q = _init_nqueens_solve_num_solutions() MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), 1) - MOI.optimize!(model) - return _check_result(model, q, 1) + _test_nqueens_solve_num_solutions(model, q, 1) + return end -function test_nqueens_throw() # test throw - @info "test throw" - model, _ = _init_model() - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - -1, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - 0, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - 1.1, - ) - @test_throws ErrorException MOI.set( - model, - MOI.RawOptimizerAttribute("num_solutions"), - "two", - ) +function test_nqueens_num_solutions_throw() + model, _ = _init_nqueens_solve_num_solutions() + for value in (-1, 0, 1.1, "two") + @test_throws( + MOI.SetAttributeNotAllowed, + MOI.set(model, MOI.RawOptimizerAttribute("num_solutions"), -1), + ) + end + return end From 4214ee8b1feb6477536fc116f3b4aa3a19dfabe5 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 22 Sep 2023 15:43:21 +1200 Subject: [PATCH 12/13] Update src/optimize.jl --- src/optimize.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/optimize.jl b/src/optimize.jl index 853adee..0e2b3e7 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -202,7 +202,6 @@ function MOI.optimize!(dest::Optimizer{T}, src::MOI.ModelLike) where {T} push!(dest.primal_solutions, copy(primal_solution)) empty!(primal_solution) end - continue elseif m_var[1] == "_objective" dest.primal_objective = _parse_result(T, m_var[2]) else From 4acf817b7a61fc90338e3722ce5d9fc1dafac802 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 22 Sep 2023 15:47:40 +1200 Subject: [PATCH 13/13] Update src/optimize.jl --- src/optimize.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/optimize.jl b/src/optimize.jl index 0e2b3e7..7c90a7a 100644 --- a/src/optimize.jl +++ b/src/optimize.jl @@ -132,7 +132,8 @@ end function MOI.set(model::Optimizer, attr::MOI.RawOptimizerAttribute, value) if attr.name == "num_solutions" && !(value isa Int && value >= 1) - throw(MOI.SetAttributeNotAllowed("value must be an `Int` that is >= 1")) + msg = "value must be an `Int` that is >= 1" + throw(MOI.SetAttributeNotAllowed(attr, msg)) end model.options[attr.name] = value return