Skip to content

Commit

Permalink
MOI tests: intlinear (#533)
Browse files Browse the repository at this point in the history
* add intlinear tests

* delete UNKNOWN_TERMINATION_STATUS and fix user_algorithms_tests
  • Loading branch information
laradicp authored May 24, 2021
1 parent 5c36414 commit c6aba41
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 98 deletions.
1 change: 0 additions & 1 deletion src/Algorithm/basic/solveipform.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ function run!(algo::SolveIpForm, env::Env, data::ModelData, input::OptimizationI
if !ip_supported
@warn "Optimizer of formulation with id =", getuid(form),
" does not support integer variables. Skip SolveIpForm algorithm."
setterminationstatus!(result, UNKNOWN_TERMINATION_STATUS)
return OptimizationOutput(result)
end

Expand Down
10 changes: 5 additions & 5 deletions src/Algorithm/utilities/optimizationstate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ end
"""
OptimizationState(
form;
termination_status = UNKNOWN_TERMINATION_STATUS,
termination_status = OPTIMIZE_NOT_CALLED,
ip_primal_bound = nothing,
ip_dual_bound = nothing,
lp_primal_bound = nothing,
Expand All @@ -49,9 +49,9 @@ end
insert_function_lp_dual_sols = bestbound!
)
A convenient structure to maintain and return solutions and bounds of a formulation `form` during an
optimization process. The termination statuses is considered as
unknown by default. You can define the initial incumbent bounds using `ip_primal_bound`,
A convenient structure to maintain and return solutions and bounds of a formulation
`form` during an optimization process. The termination status is `OPTIMIZE_NOT_CALLED`
by default. You can define the initial incumbent bounds using `ip_primal_bound`,
`ip_dual_bound`, `lp_primal_bound`, and `lp_primal_bound` keyword arguments. Incumbent
bounds are set to infinite (according to formulation objective sense) by default.
You can store three types of solutions `ip_primal_sols`, `lp_primal_sols`, and `lp_dual_sols`.
Expand All @@ -64,7 +64,7 @@ best bound.
"""
function OptimizationState(
form::F;
termination_status::TerminationStatus = UNKNOWN_TERMINATION_STATUS,
termination_status::TerminationStatus = OPTIMIZE_NOT_CALLED,
ip_primal_bound = nothing,
ip_dual_bound = nothing,
lp_primal_bound = nothing,
Expand Down
4 changes: 2 additions & 2 deletions src/ColunaBase/ColunaBase.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export Bound, Solution, getvalue, isbetter, diff, gap, printbounds, getsol,
getstatus, remove_until_last_point

# Statuses
export TerminationStatus, SolutionStatus, OPTIMAL, INFEASIBLE, TIME_LIMIT,
NODE_LIMIT, OTHER_LIMIT, UNKNOWN_TERMINATION_STATUS, UNCOVERED_TERMINATION_STATUS,
export TerminationStatus, SolutionStatus, OPTIMIZE_NOT_CALLED, OPTIMAL,
INFEASIBLE, TIME_LIMIT, NODE_LIMIT, OTHER_LIMIT, UNCOVERED_TERMINATION_STATUS,
FEASIBLE_SOL, INFEASIBLE_SOL, UNKNOWN_FEASIBILITY, UNKNOWN_SOLUTION_STATUS,
UNCOVERED_SOLUTION_STATUS, convert_status

Expand Down
16 changes: 9 additions & 7 deletions src/ColunaBase/solsandbounds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -133,24 +133,24 @@ MOI [`TerminationStatusCode`](https://jump.dev/MathOptInterface.jl/stable/apiref
is translated into a Coluna `TerminationStatus`.
Description of the termination statuses:
- `OPTIMAL` : the algorithm found a global optimal solution given the optimality tolerance.
- `OPTIMAL` : the algorithm found a global optimal solution given the optimality tolerance
- `INFEASIBLE` : the algorithm proved infeasibility
- `TIME_LIMIT` : the algorithm stopped because of the time limit
- `NODE_LIMIT` : the branch-and-bound based algorithm stopped due to the node limit
- `OTHER_LIMIT` : the algorithm stopped because of a limit that is neither the time limit
nor the node limit
If the algorithm has not been called, the default value of the termination status should be:
- `UNKNOWN_TERMINATION_STATUS`
- `OPTIMIZE_NOT_CALLED`
If the subsolver called through MOI returns a
`TerminationStatusCode` that is not `MOI.OPTIMAL`, `MOI.INFEASIBLE`, `MOI.TIME_LIMIT`, `MOI.NODE_LIMIT`, or
`MOI.OTHER_LIMIT`:
`TerminationStatusCode` that is not `MOI.OPTIMIZE_NOT_CALLED`, `MOI.OPTIMAL`, `MOI.INFEASIBLE`,
`MOI.TIME_LIMIT`, `MOI.NODE_LIMIT`, or `MOI.OTHER_LIMIT`:
- `UNCOVERED_TERMINATION_STATUS` : should not be used by a Coluna algorithm
"""
@enum(
TerminationStatus, OPTIMAL, INFEASIBLE, TIME_LIMIT, NODE_LIMIT, OTHER_LIMIT,
UNKNOWN_TERMINATION_STATUS, UNCOVERED_TERMINATION_STATUS
TerminationStatus, OPTIMIZE_NOT_CALLED, OPTIMAL, INFEASIBLE,
TIME_LIMIT, NODE_LIMIT, OTHER_LIMIT, UNCOVERED_TERMINATION_STATUS
)

"""
Expand All @@ -162,7 +162,7 @@ Description of the solution statuses:
If there is no solution or if we don't have information about the solution, the
solution status should be :
- `UNKWNOW_SOLUTION_STATUS`
- `UNKNOWN_SOLUTION_STATUS`
"""
@enum(
Expand All @@ -180,6 +180,7 @@ Convert a termination or solution `status` of a given type to the corresponding
This method is used to communicate between Coluna and MathOptInterface.
"""
function convert_status(moi_status::MOI.TerminationStatusCode)
moi_status == MOI.OPTIMIZE_NOT_CALLED && return OPTIMIZE_NOT_CALLED
moi_status == MOI.OPTIMAL && return OPTIMAL
moi_status == MOI.INFEASIBLE && return INFEASIBLE
moi_status == MOI.TIME_LIMIT && return TIME_LIMIT
Expand All @@ -189,6 +190,7 @@ function convert_status(moi_status::MOI.TerminationStatusCode)
end

function convert_status(coluna_status::TerminationStatus)
coluna_status == OPTIMIZE_NOT_CALLED && return MOI.OPTIMIZE_NOT_CALLED
coluna_status == OPTIMAL && return MOI.OPTIMAL
coluna_status == INFEASIBLE && return MOI.INFEASIBLE
coluna_status == TIME_LIMIT && return MOI.TIME_LIMIT
Expand Down
4 changes: 3 additions & 1 deletion src/MOIwrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
constrs_on_single_var_to_vars::Dict{MOI.ConstraintIndex, VarId}
constrs_on_single_var_to_names::Dict{MOI.ConstraintIndex, String}
names_to_constrs::Dict{String, MOI.ConstraintIndex}
result::Union{Nothing,OptimizationState}
result::OptimizationState
default_optimizer_builder::Union{Nothing, Function}

feasibility_sense::Bool # Coluna supports only Max or Min.
Expand All @@ -47,6 +47,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
model.constrs_on_single_var_to_vars = Dict{MOI.ConstraintIndex, VarId}()
model.constrs_on_single_var_to_names = Dict{MOI.ConstraintIndex, String}()
model.names_to_constrs = Dict{String, MOI.ConstraintIndex}()
model.result = OptimizationState(get_optimization_target(model.inner))
model.default_optimizer_builder = nothing
model.feasibility_sense = false
return model
Expand Down Expand Up @@ -587,6 +588,7 @@ function MOI.empty!(model::Coluna.Optimizer)
if model.default_optimizer_builder !== nothing
set_default_optimizer_builder!(model.inner, model.default_optimizer_builder)
end
model.result = OptimizationState(get_optimization_target(model.inner))
return
end

Expand Down
4 changes: 3 additions & 1 deletion src/MathProg/MOIinterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ function enforce_kind_in_optimizer!(form::Formulation, v::Variable)
moirecord = getmoirecord(v)
moi_kind = getkind(moirecord)
if moi_kind.value != -1
MOI.delete(inner, moi_kind)
if MOI.is_valid(inner, moi_kind)
MOI.delete(inner, moi_kind)
end
setkind!(moirecord, MoiVarKind())
end
if kind != Continuous # Continuous is translated as no constraint in MOI
Expand Down
164 changes: 83 additions & 81 deletions test/MathOptInterface/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,84 +10,12 @@ const CONFIG = MOIT.TestConfig(atol=1e-6, rtol=1e-6)
@test MOI.get(OPTIMIZER, MOI.SolverName()) == "Coluna"
end

@testset "Kpis" begin
data = CLD.GeneralizedAssignment.data("smallgap3.txt")

coluna = JuMP.optimizer_with_attributes(
CL.Optimizer,
"params" => CL.Params(
solver = ClA.TreeSearchAlgorithm(
conqueralg = ClA.ColCutGenConquer(
colgen = ClA.ColumnGeneration(max_nb_iterations = 8)
), maxnumnodes = 4
)
),
"default_optimizer" => GLPK.Optimizer
)

problem, x, dec = CLD.GeneralizedAssignment.model(data, coluna)

JuMP.optimize!(problem)

@test MOI.get(problem, MOI.NodeCount()) == 4
@test isa(MOI.get(problem, MOI.SolveTime()), Float64)
end

@testset "supports_default_copy_to" begin
@test MOIU.supports_default_copy_to(OPTIMIZER, false)
# Use `@test !...` if names are not supported
@test MOIU.supports_default_copy_to(OPTIMIZER, true)
end

@testset "branching_priority" begin
coluna = JuMP.optimizer_with_attributes(
Coluna.Optimizer,
"params" => CL.Params(solver=ClA.SolveIpForm()),
"default_optimizer" => GLPK.Optimizer
)
model = BlockModel(coluna, direct_model=true)
@variable(model, x)

@test BlockDecomposition.branchingpriority(model, x) == 1
BlockDecomposition.branchingpriority!(model, x, 2)
@test BlockDecomposition.branchingpriority(model, x) == 2

model2 = BlockModel(coluna)
@variable(model2, x)

@test BlockDecomposition.branchingpriority(model2, x) == 1
BlockDecomposition.branchingpriority!(model2, x, 2)
@test BlockDecomposition.branchingpriority(model2, x) == 2
end

@testset "write_to_file" begin
coluna = JuMP.optimizer_with_attributes(
Coluna.Optimizer,
"params" => CL.Params(solver=ClA.SolveIpForm()),
"default_optimizer" => GLPK.Optimizer
)
model = BlockModel(coluna, direct_model=true)
@variable(model, x)

@constraint(model, x <= 1)
@objective(model, Max, x)
optimize!(model)

dest = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_MPS)
MOI.copy_to(dest, model)
MOI.write_to_file(dest, "model.mps")

filedata = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_MPS)
MOI.read_from_file(filedata, "model.mps")

model2 = BlockModel(coluna, direct_model=true)
MOI.copy_to(model2, filedata)
optimize!(model2)

# the model is always written as a minimization problem
@test JuMP.objective_value(model) == -JuMP.objective_value(model2)
end

const UNSUPPORTED_TESTS = [
"solve_qcp_edge_cases", # Quadratic constraints not supported
"delete_nonnegative_variables", # variable deletion not supported
Expand Down Expand Up @@ -161,13 +89,18 @@ const LP_TESTS = [
MOIT.unittest(OPTIMIZER, CONFIG, vcat(UNSUPPORTED_TESTS, LP_TESTS, BASIC))
end

# @testset "Integer Linear" begin
# MOIT.intlineartest(OPTIMIZER, CONFIG, [
# "indicator1", "indicator2", "indicator3", "indicator4", # indicator constraints not supported
# "semiconttest", "semiinttest", # semi integer vars not supported
# "int2" # SOS1 & SOS2 not supported
# ])
# end
const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes(Coluna.Optimizer)#, MOI.Silent() => true) # MOI.Silent not supported
const BRIDGED = MOI.instantiate(OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64)
MOI.set(BRIDGED, MOI.RawParameter("default_optimizer"), GLPK.Optimizer)
MOI.set(BRIDGED, MOI.RawParameter("params"), CL.Params(solver = ClA.SolveIpForm()))

@testset "Integer Linear" begin
MOIT.intlineartest(BRIDGED, CONFIG, [
"indicator1", "indicator2", "indicator3", "indicator4", # indicator constraints not supported
"semiconttest", "semiinttest", # semi integer vars not supported
"int2" # SOS1 & SOS2 not supported
])
end

# @testset "Unit LP" begin
# MOI.set(OPTIMIZER, MOI.RawParameter("params"), CL.Params(solver = ClA.SolveLpForm()))
Expand All @@ -180,5 +113,74 @@ end
# ])
# end

const OPTIMIZER_CONSTRUCTOR = MOI.OptimizerWithAttributes(Coluna.Optimizer)#, MOI.Silent() => true) # MOI.Silent not supported
const BRIDGED = MOI.instantiate(OPTIMIZER_CONSTRUCTOR, with_bridge_type = Float64)
@testset "Kpis" begin
data = CLD.GeneralizedAssignment.data("smallgap3.txt")

coluna = JuMP.optimizer_with_attributes(
CL.Optimizer,
"params" => CL.Params(
solver = ClA.TreeSearchAlgorithm(
conqueralg = ClA.ColCutGenConquer(
colgen = ClA.ColumnGeneration(max_nb_iterations = 8)
), maxnumnodes = 4
)
),
"default_optimizer" => GLPK.Optimizer
)

problem, x, dec = CLD.GeneralizedAssignment.model(data, coluna)

JuMP.optimize!(problem)

@test MOI.get(problem, MOI.NodeCount()) == 4
@test isa(MOI.get(problem, MOI.SolveTime()), Float64)
end

@testset "branching_priority" begin
coluna = JuMP.optimizer_with_attributes(
Coluna.Optimizer,
"params" => CL.Params(solver=ClA.SolveIpForm()),
"default_optimizer" => GLPK.Optimizer
)
model = BlockModel(coluna, direct_model=true)
@variable(model, x)

@test BlockDecomposition.branchingpriority(model, x) == 1
BlockDecomposition.branchingpriority!(model, x, 2)
@test BlockDecomposition.branchingpriority(model, x) == 2

model2 = BlockModel(coluna)
@variable(model2, x)

@test BlockDecomposition.branchingpriority(model2, x) == 1
BlockDecomposition.branchingpriority!(model2, x, 2)
@test BlockDecomposition.branchingpriority(model2, x) == 2
end

@testset "write_to_file" begin
coluna = JuMP.optimizer_with_attributes(
Coluna.Optimizer,
"params" => CL.Params(solver=ClA.SolveIpForm()),
"default_optimizer" => GLPK.Optimizer
)
model = BlockModel(coluna, direct_model=true)
@variable(model, x)

@constraint(model, x <= 1)
@objective(model, Max, x)
optimize!(model)

dest = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_MPS)
MOI.copy_to(dest, model)
MOI.write_to_file(dest, "model.mps")

filedata = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_MPS)
MOI.read_from_file(filedata, "model.mps")

model2 = BlockModel(coluna, direct_model=true)
MOI.copy_to(model2, filedata)
optimize!(model2)

# the model is always written as a minimization problem
@test JuMP.objective_value(model) == -JuMP.objective_value(model2)
end
2 changes: 2 additions & 0 deletions test/user_algorithms_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ function Coluna.Algorithm.run!(
heur_output = run!(algo.rm_heur, env, data, OptimizationInput(optstate))
update_all_ip_primal_solutions!(optstate, getoptstate(heur_output))

setterminationstatus!(optstate, getterminationstatus(getoptstate(heur_output)))

return OptimizationOutput(optstate)
end

Expand Down

0 comments on commit c6aba41

Please sign in to comment.