diff --git a/.travis.yml b/.travis.yml index 02b87d2..24ce1af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,11 @@ os: - osx julia: - 0.6 - - nightly + - 0.7 notifications: email: false script: - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi - julia -e 'Pkg.clone(pwd()); Pkg.build("Clp"); Pkg.test("Clp"; coverage=true)' -#after_success: - #- julia -e 'cd(Pkg.dir("Clp")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())' +after_success: + - julia -e 'cd(Pkg.dir("Clp")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' diff --git a/REQUIRE b/REQUIRE index f20e681..c3899b1 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,3 +1,5 @@ julia 0.6 +Compat 0.59 Cbc MathProgBase 0.5 0.8 +LinQuadOptInterface 0.3 0.4 diff --git a/src/Clp.jl b/src/Clp.jl index e8e5882..85d6ef7 100644 --- a/src/Clp.jl +++ b/src/Clp.jl @@ -6,9 +6,11 @@ __precompile__() module Clp +using Compat include("ClpCInterface.jl") include("ClpSolverInterface.jl") +include("MOIWrapper.jl") using Clp.ClpMathProgSolverInterface export ClpSolver diff --git a/src/ClpCInterface.jl b/src/ClpCInterface.jl index c8af204..b57aae8 100644 --- a/src/ClpCInterface.jl +++ b/src/ClpCInterface.jl @@ -19,7 +19,9 @@ export resize, delete_rows, add_rows, + add_row, delete_columns, + add_column, add_columns, chg_row_lower, chg_row_upper, @@ -414,16 +416,30 @@ function add_rows(model::ClpModel, number::Integer, row_lower::Vector{Float64}, row_starts::Vector{Int32}, columns::Vector{Int32}, elements::Vector{Float64}) _jl__check_model(model) - @clp_ccall addRows Void (Ptr{Void}, Int32, Ptr{Float64}, Ptr{Float64}, Ptr{Int32}, Ptr{Int32}, Ptr{Float64}) model.p number row_lower row_upper row_starts columns elements end +#This function exists in cpp but not c interface +function add_row(model::ClpModel, nnz::Cint, columns::Vector{Int32}, + elements::Vector{Float64}, row_lower::Float64, row_upper::Float64) + add_rows(model, 1, [row_lower], [row_upper], [Cint(0), nnz], columns, + elements) +end + # Delete columns. function delete_columns(model::ClpModel, which::Vector{Int32}) _jl__check_model(model) @clp_ccall deleteColumns Void (Ptr{Void},Int32,Ptr{Int32}) model.p length(which) which end +#This function exists in cpp but not c interface +function add_column(model::ClpModel, nnz::Int32, rows::Vector{Int32}, + elements::Vector{Float64}, column_lower::Float64, + column_upper::Float64, objective::Float64) + add_columns(model, 1, [column_lower], [column_upper], [objective], + [Int32(0), nnz], rows, elements) +end + # Add columns. function add_columns(model::ClpModel, number::Integer, column_lower::Vector{Float64}, column_upper::Vector{Float64}, @@ -440,7 +456,6 @@ function add_columns(model::ClpModel, column_lower::Vector{Float64}, column_upper::Vector{Float64}, objective::Vector{Float64}, new_columns::SparseMatrixCSC{Float64,Int32}) - add_columns(model, new_columns.n, column_lower, column_upper, objective, new_columns.colptr-convert(Int32,1), new_columns.rowval-convert(Int32,1), new_columns.nzval) end diff --git a/src/MOIWrapper.jl b/src/MOIWrapper.jl new file mode 100644 index 0000000..408813d --- /dev/null +++ b/src/MOIWrapper.jl @@ -0,0 +1,370 @@ + +using LinQuadOptInterface +using .ClpCInterface + +const LQOI = LinQuadOptInterface +const MOI = LQOI.MOI + +const SUPPORTED_OBJECTIVES = [ + LQOI.Linear, + LQOI.SinVar +] + +const SUPPORTED_CONSTRAINTS = [ + (LQOI.Linear, LQOI.EQ), + (LQOI.Linear, LQOI.LE), + (LQOI.Linear, LQOI.GE), + (LQOI.SinVar, LQOI.EQ), + (LQOI.SinVar, LQOI.LE), + (LQOI.SinVar, LQOI.GE), + (LQOI.SinVar, LQOI.IV), + (LQOI.VecVar, MOI.Nonnegatives), + (LQOI.VecVar, MOI.Nonpositives), + (LQOI.VecVar, MOI.Zeros), + (LQOI.VecLin, MOI.Nonnegatives), + (LQOI.VecLin, MOI.Nonpositives), + (LQOI.VecLin, MOI.Zeros) +] + +mutable struct Optimizer <: LQOI.LinQuadOptimizer + LQOI.@LinQuadOptimizerBase + params::Dict{Symbol,Any} + Optimizer(::Void) = new() +end + +### Options + +# map option name to C function +const optionmap = Dict( + :PrimalTolerance => set_primal_tolerance, + :DualTolerance => set_dual_tolerance, + :DualObjectiveLimit => set_dual_objective_limit, + :MaximumIterations => set_maximum_iterations, + :MaximumSeconds => set_maximum_seconds, + :LogLevel => set_log_level, + :Scaling => scaling, + :Perturbation => set_perturbation, + ) +# These options are set by using the ClpSolve object +const solveoptionmap = Dict( + :PresolveType => set_presolve_type, + :SolveType => set_solve_type, + :InfeasibleReturn => set_infeasible_return, + ) + +function Optimizer(;kwargs...) + optimizer = Optimizer(nothing) + optimizer.params = Dict{String,Any}() + MOI.empty!(optimizer) + for (name,value) in kwargs + optimizer.params[Symbol(name)] = value + end + return optimizer +end + +LQOI.LinearQuadraticModel(::Type{Optimizer},env) = ClpModel() + +LQOI.supported_constraints(optimizer::Optimizer) = SUPPORTED_CONSTRAINTS +LQOI.supported_objectives(optimizer::Optimizer) = SUPPORTED_OBJECTIVES + +""" + replace_inf(x::Real) + +Return `Inf` if `x>1e20`, `-Inf` if `x<-1e20`, and `x` otherwise. +""" +function replace_inf(x::Real) + if x > 1e20 + return Inf + elseif x < -1e20 + return -Inf + else + return x + end +end + +function LQOI.change_variable_bounds!(instance::Optimizer, cols::Vector{Int}, + values::Vector{Float64}, senses::Vector) + upperbounds = get_col_upper(instance.inner) + lowerbounds = get_col_lower(instance.inner) + for (col, value, sense) in zip(cols, values, senses) + if sense == Cchar('U') + upperbounds[col] = value + elseif sense == Cchar('L') + lowerbounds[col] = value + else + error("sense is Cchar('$(Char(sense))'), but only Cchar('U') " * + "Cchar('L') are supported.") + end + end + chg_column_upper(instance.inner, upperbounds) + chg_column_lower(instance.inner, lowerbounds) +end + +function LQOI.get_variable_lowerbound(instance::Optimizer, col::Int) + lower = get_col_lower(instance.inner) + return replace_inf(lower[col]) +end + +function LQOI.get_variable_upperbound(instance::Optimizer, col::Int) + upper = get_col_upper(instance.inner) + return replace_inf(upper[col]) +end + +function LQOI.get_number_linear_constraints(instance::Optimizer) + return get_num_rows(instance.inner) +end + +""" + append_row(instance::Optimizer, row::Int, lower::Float64, upper::Float64, + rows::Vector{Int}, cols::Vector{Int}, coefs::Vector{Float64}) + +Given a sparse matrix in the triplet-form `(rows, cols, coefs)`, add row `row` +with upper bound `upper` and lower bound `lower` to the instance `instance`. +""" +function append_row(instance::Optimizer, row::Int, lower::Float64, + upper::Float64, rows::Vector{Int}, cols::Vector{Int}, + coefs::Vector{Float64}) + indices = if row == length(rows) + rows[row]:length(cols) + else + rows[row]:(rows[row+1]-1) + end + add_row(instance.inner, Cint(length(indices)), Cint.(cols[indices]-1), + coefs[indices], lower, upper) +end + +function LQOI.add_linear_constraints!(instance::Optimizer, A::LQOI.CSRMatrix{Float64}, + senses::Vector{Cchar}, right_hand_sides::Vector{Float64}) + rows = A.row_pointers + cols = A.columns + coefs = A.coefficients + for (row, (rhs, sense)) in enumerate(zip(right_hand_sides, senses)) + if rhs > 1e20 + error("rhs must always be less than 1e20") + elseif rhs < -1e20 + error("rhs must always be greater than -1e20") + end + lower = -Inf + upper = Inf + if sense == Cchar('L') + upper = rhs + elseif sense == Cchar('G') + lower = rhs + elseif sense == Cchar('E') + upper = lower = rhs + else + error("sense must be Cchar(x) where x is in ['L','G',E']") + end + append_row(instance, row, lower, upper, rows, cols, coefs) + end +end + +function LQOI.add_ranged_constraints!(instance::Clp.Optimizer, + A::LinQuadOptInterface.CSRMatrix{Float64}, + lb::Vector{Float64}, ub::Vector{Float64}) + rows = A.row_pointers + cols = A.columns + coefs = A.coefficients + for row in 1:length(lb) + append_row(instance, row, lb[row], ub[row], rows, cols, coefs) + end +end + +function LQOI.get_rhs(instance::Optimizer, row::Int) + lower_bounds = get_row_lower(instance.inner) + upper_bounds = get_row_upper(instance.inner) + lower_bound = replace_inf(lower_bounds[row]) + upper_bound = replace_inf(upper_bounds[row]) + if lower_bound > -Inf + return lower_bound + elseif upper_bound < Inf + return upper_bound + else + error("Either row_lower or row_upper must be of abs less than 1e20") + end +end + +function LQOI.get_linear_constraint(instance::Optimizer, row::Int)::Tuple{Vector{Int}, Vector{Float64}} + A = get_constraint_matrix(instance.inner) + A_row = A[row,:] + return Array{Int}(A_row.nzind), A_row.nzval +end + +function LQOI.change_objective_coefficient!(instance::Optimizer, col, coef) + objcoefs = get_obj_coefficients(instance.inner) + objcoefs[col] = coef + chg_obj_coefficients(instance.inner, objcoefs) +end + +function LQOI.change_rhs_coefficient!(instance::Optimizer, row, coef) + lower_bounds = get_row_lower(instance.inner) + upper_bounds = get_row_upper(instance.inner) + lower_bound = replace_inf(lower_bounds[row]) + upper_bound = replace_inf(upper_bounds[row]) + if lower_bound > -Inf + lower_bounds[row] = coef + chg_row_lower(instance.inner, lower_bounds) + elseif upper_bound < Inf + upper_bounds[row] = coef + chg_row_upper(instance.inner, upper_bounds) + else + error("Either row_lower or row_upper must be of abs less than 1e20") + end +end + +function LQOI.delete_linear_constraints!(instance::Optimizer, start_row::Int, end_row::Int) + delete_rows(instance.inner, [Cint(i-1) for i in start_row:end_row]) +end + +function LQOI.change_linear_constraint_sense!(instance::Optimizer, rows::Vector{Int}, senses::Vector{Cchar}) + lower = replace_inf.(get_row_lower(instance.inner)) + upper = replace_inf.(get_row_upper(instance.inner)) + for (sense, row) in zip(senses, rows) + lb = lower[row] + ub = upper[row] + if lb > -Inf + rhs = lb + elseif ub < Inf + rhs = ub + else + error("Either row_lower or row_upper must be of abs less than 1e20") + end + if sense == Cchar('G') + lower[row] = rhs + upper[row] = Inf + elseif sense == Cchar('L') + lower[row] = -Inf + upper[row] = rhs + elseif sense == Cchar('E') + lower[row] = rhs + upper[row] = rhs + end + end + chg_row_upper(instance.inner, upper) + chg_row_lower(instance.inner, lower) +end + +function LQOI.set_linear_objective!(instance::Optimizer, cols::Vector{Int}, coefs::Vector{Float64}) + objective_coefficients = zeros(Float64, get_num_cols(instance.inner)) + for (col, coef) in zip(cols, coefs) + objective_coefficients[col] += coef + end + chg_obj_coefficients(instance.inner, objective_coefficients) +end + +function LQOI.change_objective_sense!(instance::Optimizer, sense::Symbol) + if sense == :min + set_obj_sense(instance.inner, 1.0) + elseif sense == :max + set_obj_sense(instance.inner, -1.0) + else + error("sense must be either :min or :max") + end +end + +function LQOI.get_linear_objective!(instance::Optimizer, x::Vector{Float64}) + copy!(x, get_obj_coefficients(instance.inner)) +end + +function LQOI.solve_linear_problem!(instance::Optimizer) + solveroptions = ClpSolve() + model = instance.inner + for (name, value) in instance.params + if haskey(optionmap, name) + optionmap[name](model, value) + elseif haskey(solveoptionmap, name) + solveoptionmap[name](solveroptions,value) + else + error("Unrecognized option: $name") + end + end + initial_solve_with_options(instance.inner, solveroptions) +end + +function LQOI.get_variable_primal_solution!(instance::Optimizer, x::Vector{Float64}) + copy!(x, primal_column_solution(instance.inner)) +end + +function LQOI.get_linear_primal_solution!(instance::Optimizer, x::Vector{Float64}) + copy!(x, primal_row_solution(instance.inner)) +end + +function LQOI.get_variable_dual_solution!(instance::Optimizer, x::Vector{Float64}) + copy!(x, dual_column_solution(instance.inner)) +end + +function LQOI.get_linear_dual_solution!(instance::Optimizer, x::Vector{Float64}) + copy!(x, dual_row_solution(instance.inner)) +end + +function LQOI.get_objective_value(instance::Optimizer) + return objective_value(instance.inner) +end + +function LQOI.get_farkas_dual!(instance::Optimizer, result::Vector{Float64}) + copy!(result, infeasibility_ray(instance.inner)) + scale!(result, -1.0) +end + +function LQOI.get_unbounded_ray!(instance::Optimizer, result::Vector{Float64}) + copy!(result, unbounded_ray(instance.inner)) +end + +function LQOI.get_termination_status(instance::Optimizer) + status = ClpCInterface.status(instance.inner) + if status == 0 + return MOI.Success + elseif status == 1 + if is_proven_primal_infeasible(instance.inner) + return MOI.Success + else + return MOI.InfeasibleNoResult + end + elseif status == 2 + if is_proven_dual_infeasible(instance.inner) + return MOI.Success + else + return MOI.UnboundedNoResult + end + elseif status == 3 + return MOI.OtherLimit + elseif status == 4 + return MOI.OtherError + else + error("status returned was $(status), but it must be in [0,1,2,3,4]") + end +end + +function LQOI.get_primal_status(instance::Optimizer) + if is_proven_dual_infeasible(instance.inner) + return MOI.InfeasibilityCertificate + elseif primal_feasible(instance.inner) + return MOI.FeasiblePoint + else + return MOI.UnknownResultStatus + end +end + +function LQOI.get_dual_status(instance::Optimizer) + if is_proven_primal_infeasible(instance.inner) + return MOI.InfeasibilityCertificate + elseif dual_feasible(instance.inner) + return MOI.FeasiblePoint + else + return MOI.UnknownResultStatus + end +end + +function LQOI.get_number_variables(instance::Optimizer) + return get_num_cols(instance.inner) +end + +function LQOI.add_variables!(instance::Optimizer, number_of_variables::Int) + for i in 1:number_of_variables + add_column(instance.inner, Cint(0), Int32[], Float64[], -Inf, Inf, 0.0) + end +end + +function LQOI.delete_variables!(instance::Optimizer, start_col::Int, end_col::Int) + delete_columns(instance.inner, [Cint(i-1) for i in start_col:end_col]) +end diff --git a/test/MOIWrapper.jl b/test/MOIWrapper.jl new file mode 100644 index 0000000..20e2a66 --- /dev/null +++ b/test/MOIWrapper.jl @@ -0,0 +1,57 @@ +using MathOptInterface + +const MOI = MathOptInterface +const MOIB = MathOptInterface.Bridges +const MOIT = MathOptInterface.Test + +@testset "Unit Tests" begin + config = MOIT.TestConfig() + solver = Clp.Optimizer(LogLevel = 0) + MOIT.basic_constraint_tests(solver, config) + MOIT.unittest(solver, config, [ + "solve_qp_edge_cases", # unsupported + "solve_qcp_edge_cases", # unsupported + "solve_affine_interval", # unsupported + "solve_objbound_edge_cases", # unsupported integer variables + "solve_integer_edge_cases", # unsupported integer variables + ]) +end + +@testset "Linear tests" begin + linconfig = MOIT.TestConfig(modify_lhs = false) + solver = Clp.Optimizer(LogLevel = 0) + MOIT.contlineartest(solver, linconfig, [ + # linear1 test is disabled due to the following bug: + # https://projects.coin-or.org/Clp/ticket/84 + "linear1", + # linear10 test is tested below because it has interval sets. + "linear10", + # linear11 test is excluded as it fails on Linux for some reason. + # It passes on Mac and Windows. + "linear11", + # linear12 test requires the InfeasibilityCertificate for variable + # bounds. These are available through C++, but not via the C interface. + "linear12" + ]) + + @testset "Interval Bridge" begin + MOIT.linear10test(MOIB.SplitInterval{Float64}(solver), linconfig) + end +end + +@testset "ModelLike tests" begin + solver = Clp.Optimizer(LogLevel = 0) + @testset "nametest" begin + MOIT.nametest(solver) + end + @testset "validtest" begin + MOIT.validtest(solver) + end + @testset "emptytest" begin + MOIT.emptytest(solver) + end + @testset "copytest" begin + solver2 = Clp.Optimizer(LogLevel = 0) + MOIT.copytest(solver,solver2) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 1aeb744..8febfc6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,9 @@ -using Clp +using Clp, Base.Test -include("mathprog.jl") +@testset "MathProgBase" begin + include("mathprog.jl") +end + +@testset "MathOptInterface" begin + include("MOIWrapper.jl") +end