diff --git a/Project.toml b/Project.toml index 4e69f77..236f600 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.3.0" CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0" Conda = "8f4d0f93-b110-5947-807f-2305c1781a2d" HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" diff --git a/deps/build.jl b/deps/build.jl index 41239bb..cb0f016 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -5,7 +5,7 @@ function install_miplearn() Conda.update() pip = joinpath(dirname(pyimport("sys").executable), "pip") isfile(pip) || error("$pip: invalid path") - run(`$pip install miplearn==0.2.0.dev13`) + run(`$pip install miplearn==0.3.0.dev0`) end install_miplearn() diff --git a/src/Cuts/BlackBox/cplex.jl b/src/Cuts/BlackBox/cplex.jl index 7a522f0..385482e 100644 --- a/src/Cuts/BlackBox/cplex.jl +++ b/src/Cuts/BlackBox/cplex.jl @@ -4,7 +4,6 @@ using CPLEX using JuMP -using HDF5 Base.@kwdef struct CplexBlackBoxCuts threads::Int = 1 @@ -26,10 +25,7 @@ function _add_mip_start!(env, lp, x::Vector{Float32}) rval == 0 || error("CPXaddmipstarts failed: $rval") end -function collect( - mps_filename::String, - method::CplexBlackBoxCuts, -)::Nothing +function collect(mps_filename::String, method::CplexBlackBoxCuts)::Nothing tempdir = mktempdir() isfile(mps_filename) || error("file not found: $mps_filename") h5_filename = replace(mps_filename, ".mps.gz" => ".h5") @@ -47,8 +43,8 @@ function collect( CPXsetintparam(env, CPX_PARAM_PREDUAL, -1) CPXsetintparam(env, CPX_PARAM_PRESLVND, -1) - # Parameter: Enable logging - CPXsetintparam(env, CPX_PARAM_SCRIND, 1) + # Parameter: Disable logging + CPXsetintparam(env, CPX_PARAM_SCRIND, 0) # Parameter: Stop processing at the root node CPXsetintparam(env, CPX_PARAM_NODELIM, 0) @@ -68,7 +64,7 @@ function collect( CPXreadcopyprob(env, lp, mps_filename, "mps") # Load warm start - h5 = Hdf5Sample(h5_filename) + h5 = H5File(h5_filename) var_values = h5.get_array("mip_var_values") h5.file.close() _add_mip_start!(env, lp, var_values) @@ -80,13 +76,17 @@ function collect( CPXwriteprob(env, nodelp_p[1], "$tempdir/root.mps", C_NULL) return 0 end - c_solve_callback = @cfunction($solve_callback, Cint, ( - CPXENVptr, # env - Ptr{Cvoid}, # cbdata - Cint, # wherefrom - Ptr{Cvoid}, # cbhandle - Ptr{Cint}, # useraction_p - )) + c_solve_callback = @cfunction( + $solve_callback, + Cint, + ( + CPXENVptr, # env + Ptr{Cvoid}, # cbdata + Cint, # wherefrom + Ptr{Cvoid}, # cbhandle + Ptr{Cint}, # useraction_p + ) + ) CPXsetsolvecallbackfunc(env, c_solve_callback, C_NULL) # Run optimization @@ -96,18 +96,20 @@ function collect( model = JuMP.read_from_file("$tempdir/root.mps") function select(cr) - return name(cr)[begin] in ['i', 'f', 'm', 'r', 'L', 'z', 'v'] && isdigit(name(cr)[begin+1]) + return name(cr)[begin] in ['i', 'f', 'm', 'r', 'L', 'z', 'v'] && + isdigit(name(cr)[begin+1]) end # Parse cuts - constraints = all_constraints(model, GenericAffExpr{Float64,VariableRef}, MOI.LessThan{Float64}) + constraints = + all_constraints(model, GenericAffExpr{Float64,VariableRef}, MOI.LessThan{Float64}) nvars = num_variables(model) ncuts = length([cr for cr in constraints if select(cr)]) cuts_lhs = spzeros(ncuts, nvars) cuts_rhs = Float64[] cuts_var_names = String[] - for i in 1:nvars + for i = 1:nvars push!(cuts_var_names, name(VariableRef(model, MOI.VariableIndex(i)))) end @@ -121,20 +123,18 @@ function collect( if (idx < 1 || idx > nvars) error("invalid index: $idx") end - cuts_lhs[offset, idx - 1] = val + cuts_lhs[offset, idx-1] = val end push!(cuts_rhs, cset.upper) offset += 1 end end - - @info "Storing $(length(cuts_rhs)) CPLEX cuts..." - h5 = Hdf5Sample(h5_filename) + + h5 = H5File(h5_filename) h5.put_sparse("cuts_cpx_lhs", cuts_lhs) h5.put_array("cuts_cpx_rhs", cuts_rhs) h5.put_array("cuts_cpx_var_names", to_str_array(cuts_var_names)) - h5.file.close() - + h5.close() return end diff --git a/src/MIPLearn.jl b/src/MIPLearn.jl index 3cb303a..1aff886 100644 --- a/src/MIPLearn.jl +++ b/src/MIPLearn.jl @@ -7,44 +7,15 @@ module MIPLearn using PyCall using SparseArrays -global miplearn = PyNULL() -global Hdf5Sample = PyNULL() - -to_str_array(values) = py"to_str_array"(values) - -from_str_array(values) = py"from_str_array"(values) +include("problems/setcover.jl") +include("io.jl") +include("solvers/jump.jl") +include("Cuts/BlackBox/cplex.jl") function __init__() - copy!(miplearn, pyimport("miplearn")) - copy!(Hdf5Sample, miplearn.features.sample.Hdf5Sample) - - py""" - import numpy as np - - def to_str_array(values): - if values is None: - return None - return np.array(values, dtype="S") - - def from_str_array(values): - return [v.decode() for v in values] - """ + __init_problems_setcover__() + __init_io__() + __init_solvers_jump__() end -function convert(::Type{SparseMatrixCSC}, o::PyObject) - I, J, V = pyimport("scipy.sparse").find(o) - return sparse(I .+ 1, J .+ 1, V, o.shape...) -end - -function PyObject(m::SparseMatrixCSC) - pyimport("scipy.sparse").csc_matrix( - (m.nzval, m.rowval .- 1, m.colptr .- 1), - shape = size(m), - ).tocoo() -end - -include("Cuts/BlackBox/cplex.jl") - -export Hdf5Sample - -end # module \ No newline at end of file +end # module diff --git a/src/io.jl b/src/io.jl new file mode 100644 index 0000000..28fd350 --- /dev/null +++ b/src/io.jl @@ -0,0 +1,35 @@ +global H5File = PyNULL() + +to_str_array(values) = py"to_str_array"(values) + +from_str_array(values) = py"from_str_array"(values) + +function __init_io__() + copy!(H5File, pyimport("miplearn.h5").H5File) + + py""" + import numpy as np + + def to_str_array(values): + if values is None: + return None + return np.array(values, dtype="S") + + def from_str_array(values): + return [v.decode() for v in values] + """ +end + +function convert(::Type{SparseMatrixCSC}, o::PyObject) + I, J, V = pyimport("scipy.sparse").find(o) + return sparse(I .+ 1, J .+ 1, V, o.shape...) +end + +function PyObject(m::SparseMatrixCSC) + pyimport("scipy.sparse").csc_matrix( + (m.nzval, m.rowval .- 1, m.colptr .- 1), + shape = size(m), + ).tocoo() +end + +export H5File diff --git a/src/problems/setcover.jl b/src/problems/setcover.jl new file mode 100644 index 0000000..7fd65d6 --- /dev/null +++ b/src/problems/setcover.jl @@ -0,0 +1,28 @@ +global SetCoverData = PyNULL() +global SetCoverGenerator = PyNULL() + +using JuMP +using HiGHS + +function __init_problems_setcover__() + copy!(SetCoverData, pyimport("miplearn.problems.setcover").SetCoverData) + copy!(SetCoverGenerator, pyimport("miplearn.problems.setcover").SetCoverGenerator) +end + +function build_setcover_model(data; optimizer = HiGHS.Optimizer) + model = Model(optimizer) + set_silent(model) + n_elements, n_sets = size(data.incidence_matrix) + E = 0:n_elements-1 + S = 0:n_sets-1 + @variable(model, x[S], Bin) + @objective(model, Min, sum(data.costs .* x)) + @constraint( + model, + eqs[e in E], + sum(data.incidence_matrix[e+1, s+1] * x[s] for s in S) >= 1 + ) + return JumpModel(model) +end + +export SetCoverData, SetCoverGenerator, build_setcover_model diff --git a/src/solvers/jump.jl b/src/solvers/jump.jl new file mode 100644 index 0000000..bfa6e51 --- /dev/null +++ b/src/solvers/jump.jl @@ -0,0 +1,290 @@ +using JuMP +using HiGHS + +global JumpModel = PyNULL() + +# ----------------------------------------------------------------------------- + +function _add_constrs( + model::JuMP.Model, + var_names, + constrs_lhs, + constrs_sense, + constrs_rhs, + stats, +) end + +function _extract_after_load(model::JuMP.Model, h5) + if JuMP.objective_sense(model) == MOI.MIN_SENSE + h5.put_scalar("static_sense", "min") + else + h5.put_scalar("static_sense", "max") + end + _extract_after_load_vars(model, h5) + _extract_after_load_constrs(model, h5) +end + +function _extract_after_load_vars(model::JuMP.Model, h5) + vars = JuMP.all_variables(model) + lb = [ + JuMP.is_binary(v) ? 0.0 : JuMP.has_lower_bound(v) ? JuMP.lower_bound(v) : -Inf + for v in vars + ] + ub = [ + JuMP.is_binary(v) ? 1.0 : JuMP.has_upper_bound(v) ? JuMP.upper_bound(v) : Inf + for v in vars + ] + types = [JuMP.is_binary(v) ? "B" : JuMP.is_integer(v) ? "I" : "C" for v in vars] + obj = objective_function(model, AffExpr) + obj_coeffs = [v ∈ keys(obj.terms) ? obj.terms[v] : 0.0 for v in vars] + h5.put_array("static_var_names", to_str_array(JuMP.name.(vars))) + h5.put_array("static_var_types", to_str_array(types)) + h5.put_array("static_var_lower_bounds", lb) + h5.put_array("static_var_upper_bounds", ub) + h5.put_array("static_var_obj_coeffs", obj_coeffs) + h5.put_scalar("static_obj_offset", obj.constant) +end + +function _extract_after_load_constrs(model::JuMP.Model, h5) + names = String[] + senses, rhs = String[], Float64[] + lhs_rows, lhs_cols, lhs_values = Int[], Int[], Float64[] + + constr_index = 1 + for (ftype, stype) in JuMP.list_of_constraint_types(model) + for constr in JuMP.all_constraints(model, ftype, stype) + cset = MOI.get(constr.model.moi_backend, MOI.ConstraintSet(), constr.index) + cf = MOI.get(constr.model.moi_backend, MOI.ConstraintFunction(), constr.index) + + name = JuMP.name(constr) + length(name) > 0 || continue + push!(names, name) + + # LHS, RHS and sense + if ftype == VariableRef + # nop + elseif ftype == AffExpr + if stype == MOI.EqualTo{Float64} + rhs_c = cset.value + push!(senses, "=") + elseif stype == MOI.LessThan{Float64} + rhs_c = cset.upper + push!(senses, "<") + elseif stype == MOI.GreaterThan{Float64} + rhs_c = cset.lower + push!(senses, ">") + else + error("Unsupported set: $stype") + end + push!(rhs, rhs_c) + for term in cf.terms + push!(lhs_cols, term.variable.value) + push!(lhs_rows, constr_index) + push!(lhs_values, term.coefficient) + end + constr_index += 1 + else + error("Unsupported constraint type: ($ftype, $stype)") + end + end + end + lhs = sparse(lhs_rows, lhs_cols, lhs_values, length(rhs), JuMP.num_variables(model)) + h5.put_sparse("static_constr_lhs", lhs) + h5.put_array("static_constr_rhs", rhs) + h5.put_array("static_constr_sense", to_str_array(senses)) + h5.put_array("static_constr_names", to_str_array(names)) +end + +function _extract_after_lp(model::JuMP.Model, h5) + h5.put_scalar("lp_wallclock_time", solve_time(model)) + h5.put_scalar("lp_obj_value", objective_value(model)) + _extract_after_lp_vars(model, h5) + _extract_after_lp_constrs(model, h5) +end + +function _extract_after_lp_vars(model::JuMP.Model, h5) + # Values and reduced costs + vars = all_variables(model) + h5.put_array("lp_var_values", JuMP.value.(vars)) + h5.put_array("lp_var_reduced_costs", reduced_cost.(vars)) + + # Basis status + basis_status = [] + for var in vars + bstatus = MOI.get(model, MOI.VariableBasisStatus(), var) + if bstatus == MOI.BASIC + bstatus_v = "B" + elseif bstatus == MOI.NONBASIC_AT_LOWER + bstatus_v = "L" + elseif bstatus == MOI.NONBASIC_AT_UPPER + bstatus_v = "U" + else + error("Unknown basis status: $(bstatus)") + end + push!(basis_status, bstatus_v) + end + h5.put_array("lp_var_basis_status", to_str_array(basis_status)) + + # Sensitivity analysis + obj_coeffs = h5.get_array("static_var_obj_coeffs") + sensitivity_report = lp_sensitivity_report(model) + sa_obj_down, sa_obj_up = Float64[], Float64[] + sa_lb_down, sa_lb_up = Float64[], Float64[] + sa_ub_down, sa_ub_up = Float64[], Float64[] + for (i, v) in enumerate(vars) + # Objective function + (delta_down, delta_up) = sensitivity_report[v] + push!(sa_obj_down, delta_down + obj_coeffs[i]) + push!(sa_obj_up, delta_up + obj_coeffs[i]) + + # Lower bound + if has_lower_bound(v) + constr = LowerBoundRef(v) + (delta_down, delta_up) = sensitivity_report[constr] + push!(sa_lb_down, lower_bound(v) + delta_down) + push!(sa_lb_up, lower_bound(v) + delta_up) + else + push!(sa_lb_down, -Inf) + push!(sa_lb_up, -Inf) + end + + # Upper bound + if has_upper_bound(v) + constr = JuMP.UpperBoundRef(v) + (delta_down, delta_up) = sensitivity_report[constr] + push!(sa_ub_down, upper_bound(v) + delta_down) + push!(sa_ub_up, upper_bound(v) + delta_up) + else + push!(sa_ub_down, Inf) + push!(sa_ub_up, Inf) + end + end + h5.put_array("lp_var_sa_obj_up", sa_obj_up) + h5.put_array("lp_var_sa_obj_down", sa_obj_down) + h5.put_array("lp_var_sa_ub_up", sa_ub_up) + h5.put_array("lp_var_sa_ub_down", sa_ub_down) + h5.put_array("lp_var_sa_lb_up", sa_lb_up) + h5.put_array("lp_var_sa_lb_down", sa_lb_down) +end + + +function _extract_after_lp_constrs(model::JuMP.Model, h5) + # Slacks + lhs = h5.get_sparse("static_constr_lhs") + rhs = h5.get_array("static_constr_rhs") + x = h5.get_array("lp_var_values") + slacks = abs.(lhs * x - rhs) + h5.put_array("lp_constr_slacks", slacks) + + sa_rhs_up, sa_rhs_down = Float64[], Float64[] + duals = Float64[] + basis_status = [] + constr_idx = 1 + sensitivity_report = lp_sensitivity_report(model) + for (ftype, stype) in JuMP.list_of_constraint_types(model) + for constr in JuMP.all_constraints(model, ftype, stype) + length(JuMP.name(constr)) > 0 || continue + + # Duals + push!(duals, JuMP.dual(constr)) + + # Basis status + b = MOI.get(model, MOI.ConstraintBasisStatus(), constr) + if b == MOI.NONBASIC + push!(basis_status, "N") + elseif b == MOI.BASIC + push!(basis_status, "B") + else + error("Unknown basis status: $b") + end + + # Sensitivity analysis + (delta_down, delta_up) = sensitivity_report[constr] + push!(sa_rhs_down, rhs[constr_idx] + delta_down) + push!(sa_rhs_up, rhs[constr_idx] + delta_up) + + constr_idx += 1 + end + end + h5.put_array("lp_constr_dual_values", duals) + h5.put_array("lp_constr_basis_status", to_str_array(basis_status)) + h5.put_array("lp_constr_sa_rhs_up", sa_rhs_up) + h5.put_array("lp_constr_sa_rhs_down", sa_rhs_down) +end + +function _extract_after_mip(model::JuMP.Model, h5) + h5.put_scalar("mip_obj_value", objective_value(model)) + h5.put_scalar("mip_obj_bound", objective_bound(model)) + h5.put_scalar("mip_wallclock_time", solve_time(model)) + h5.put_scalar("mip_gap", relative_gap(model)) + + # Values + vars = all_variables(model) + x = JuMP.value.(vars) + h5.put_array("mip_var_values", x) + + # Slacks + lhs = h5.get_sparse("static_constr_lhs") + rhs = h5.get_array("static_constr_rhs") + slacks = abs.(lhs * x - rhs) + h5.put_array("mip_constr_slacks", slacks) +end + +function _fix_variables(model::JuMP.Model, var_names, var_values, stats) end + +function _optimize(model::JuMP.Model) + optimize!(model) +end + +function _relax(model::JuMP.Model) + relaxed, _ = copy_model(model) + relax_integrality(relaxed) + # FIXME: Remove hardcoded optimizer + set_optimizer(relaxed, HiGHS.Optimizer) + set_silent(relaxed) + return relaxed +end + +function _set_warm_starts(model::JuMP.Model, var_names, var_values, stats) end + +function _write(model::JuMP.Model, filename) end + +# ----------------------------------------------------------------------------- + +function __init_solvers_jump__() + @pydef mutable struct Class + + function __init__(self, inner) + self.inner = inner + end + + add_constrs(self, var_names, constrs_lhs, constrs_sense, constrs_rhs, stats) = + _add_constrs( + self.inner, + var_names, + constrs_lhs, + constrs_sense, + constrs_rhs, + stats, + ) + + extract_after_load(self, h5) = _extract_after_load(self.inner, h5) + + extract_after_lp(self, h5) = _extract_after_lp(self.inner, h5) + + extract_after_mip(self, h5) = _extract_after_mip(self.inner, h5) + + fix_variables(self, var_names, var_values, stats) = + _fix_variables(self.inner, var_names, var_values, stats) + + optimize(self) = _optimize(self.inner) + + relax(self) = Class(_relax(self.inner)) + + set_warm_starts(self, var_names, var_values, stats) = + _set_warm_starts(self.inner, var_names, var_values, stats) + + write(self, filename) = _write(self.inner, filename) + end + copy!(JumpModel, Class) +end diff --git a/test/Project.toml b/test/Project.toml index 6639866..99916fa 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,14 @@ +name = "MIPLearnT" +uuid = "92db8938-9c6a-4af6-8bcc-af424cd0e2d5" +authors = ["Alinson S. Xavier "] +version = "0.1.0" + [deps] HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MIPLearn = "2b1277c3-b477-4c49-a15e-7ba350325c68" +PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/fixtures/bell5.h5 b/test/fixtures/bell5.h5 index 1741af8..8eb7f23 100644 Binary files a/test/fixtures/bell5.h5 and b/test/fixtures/bell5.h5 differ diff --git a/test/runtests.jl b/test/runtests.jl deleted file mode 100644 index 3312363..0000000 --- a/test/runtests.jl +++ /dev/null @@ -1,13 +0,0 @@ -# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization -# Copyright (C) 2020-2023, UChicago Argonne, LLC. All rights reserved. -# Released under the modified BSD license. See COPYING.md for more details. - -using Revise -using Test -using MIPLearn - -includet("Cuts/BlackBox/test_cplex.jl") - -function runtests() - test_cuts_blackbox_cplex() -end diff --git a/test/Cuts/BlackBox/test_cplex.jl b/test/src/Cuts/BlackBox/test_cplex.jl similarity index 84% rename from test/Cuts/BlackBox/test_cplex.jl rename to test/src/Cuts/BlackBox/test_cplex.jl index 4160a6b..0675f4a 100644 --- a/test/Cuts/BlackBox/test_cplex.jl +++ b/test/src/Cuts/BlackBox/test_cplex.jl @@ -7,14 +7,14 @@ using MIPLearn function test_cuts_blackbox_cplex() # Prepare filenames - mps_filename = joinpath(@__DIR__, "../../fixtures/bell5.mps.gz") + mps_filename = "$FIXTURES/bell5.mps.gz" h5_filename = replace(mps_filename, ".mps.gz" => ".h5") # Run collector MIPLearn.collect(mps_filename, CplexBlackBoxCuts()) # Read HDF5 file - h5 = Hdf5Sample(h5_filename) + h5 = H5File(h5_filename) rhs = h5.get_array("cuts_cpx_rhs") h5.file.close() @test length(rhs) > 0 diff --git a/test/src/MIPLearnT.jl b/test/src/MIPLearnT.jl new file mode 100644 index 0000000..6f19b08 --- /dev/null +++ b/test/src/MIPLearnT.jl @@ -0,0 +1,34 @@ +module MIPLearnT + +using Test +using Logging +using JuliaFormatter +using HiGHS + +BASEDIR = dirname(@__FILE__) +FIXTURES = "$BASEDIR/../fixtures" + +include("Cuts/BlackBox/test_cplex.jl") +include("problems/test_setcover.jl") +include("test_h5.jl") +include("solvers/test_jump.jl") + +function runtests() + @testset "MIPLearn" begin + test_cuts_blackbox_cplex() + test_h5() + test_problems_setcover() + test_solvers_jump() + end +end + +function format() + JuliaFormatter.format(BASEDIR, verbose = true) + JuliaFormatter.format("$BASEDIR/../../src", verbose = true) + return +end + + +export runtests, format + +end # module MIPLearnT diff --git a/test/src/problems/test_setcover.jl b/test/src/problems/test_setcover.jl new file mode 100644 index 0000000..e4b54cb --- /dev/null +++ b/test/src/problems/test_setcover.jl @@ -0,0 +1,56 @@ +using PyCall + +function test_problems_setcover() + test_problems_setcover_generator() + test_problems_setcover_model() +end + +function test_problems_setcover_generator() + np = pyimport("numpy") + scipy_stats = pyimport("scipy.stats") + randint = scipy_stats.randint + uniform = scipy_stats.uniform + + np.random.seed(42) + gen = SetCoverGenerator( + n_elements = randint(low = 3, high = 4), + n_sets = randint(low = 5, high = 6), + costs = uniform(loc = 0.0, scale = 100.0), + costs_jitter = uniform(loc = 0.95, scale = 0.10), + density = uniform(loc = 0.5, scale = 0), + K = uniform(loc = 25, scale = 0), + fix_sets = false, + ) + data = gen.generate(2) + @test data[1].costs == [136.75, 86.17, 25.71, 27.31, 102.48] + @test data[1].incidence_matrix == [ + 1 0 1 0 1 + 1 1 0 0 0 + 1 0 0 1 1 + ] + @test data[2].costs == [63.54, 76.6, 48.09, 74.1, 93.33] + @test data[2].incidence_matrix == [ + 1 1 0 1 1 + 0 1 0 1 0 + 0 1 1 0 0 + ] +end + +function test_problems_setcover_model() + data = SetCoverData( + costs = [5, 10, 12, 6, 8], + incidence_matrix = [ + 1 0 0 1 0 + 1 1 0 0 0 + 0 0 1 1 1 + ], + ) + + h5 = H5File(tempname(), "w") + model = build_setcover_model(data) + model.extract_after_load(h5) + model.optimize() + model.extract_after_mip(h5) + @test h5.get_scalar("mip_obj_value") == 11.0 + h5.close() +end diff --git a/test/src/solvers/test_jump.jl b/test/src/solvers/test_jump.jl new file mode 100644 index 0000000..4772f5e --- /dev/null +++ b/test/src/solvers/test_jump.jl @@ -0,0 +1,98 @@ +function build_model() + data = SetCoverData( + costs = [5, 10, 12, 6, 8], + incidence_matrix = [ + 1 0 0 1 0 + 1 1 0 0 0 + 0 0 1 1 1 + ], + ) + return build_setcover_model(data) +end + +function test_solvers_jump() + test_solvers_jump_extract() +end + +function test_solvers_jump_extract() + h5 = H5File(tempname(), "w") + + function test_scalar(key, expected) + actual = h5.get_scalar(key) + @test actual !== nothing + @test actual == expected + end + + function test_sparse(key, expected) + actual = h5.get_sparse(key) + @test actual !== nothing + @test all(actual == expected) + end + + function test_str_array(key, expected) + actual = MIPLearn.from_str_array(h5.get_array(key)) + @debug actual, expected + @test actual !== nothing + @test all(actual .== expected) + end + + + function test_array(key, expected) + actual = h5.get_array(key) + @debug actual, expected + @test actual !== nothing + @test all(actual .≈ expected) + end + + model = build_model() + model.extract_after_load(h5) + test_sparse( + "static_constr_lhs", + [ + 1 0 0 1 0 + 1 1 0 0 0 + 0 0 1 1 1 + ], + ) + test_str_array("static_constr_names", ["eqs[0]", "eqs[1]", "eqs[2]"]) + test_array("static_constr_rhs", [1, 1, 1]) + test_str_array("static_constr_sense", [">", ">", ">"]) + test_scalar("static_obj_offset", 0) + test_scalar("static_sense", "min") + test_array("static_var_lower_bounds", [0, 0, 0, 0, 0]) + test_str_array("static_var_names", ["x[0]", "x[1]", "x[2]", "x[3]", "x[4]"]) + test_array("static_var_obj_coeffs", [5, 10, 12, 6, 8]) + test_str_array("static_var_types", ["B", "B", "B", "B", "B"]) + test_array("static_var_upper_bounds", [1, 1, 1, 1, 1]) + + relaxed = model.relax() + relaxed.optimize() + relaxed.extract_after_lp(h5) + test_array("lp_constr_dual_values", [0, 10, 6]) + test_array("lp_constr_slacks", [1, 0, 0]) + test_scalar("lp_obj_value", 11) + test_array("lp_var_reduced_costs", [-5, 0, 6, 0, 2]) + test_array("lp_var_values", [1, 0, 0, 1, 0]) + test_str_array("lp_var_basis_status", ["U", "B", "L", "B", "L"]) + test_str_array("lp_constr_basis_status", ["B","N","N"]) + test_array("lp_constr_sa_rhs_up", [2, 2, 1]) + test_array("lp_constr_sa_rhs_down", [-Inf, 1, 0]) + test_array("lp_var_sa_obj_up", [10, Inf, Inf, 8, Inf]) + test_array("lp_var_sa_obj_down", [-Inf, 5, 6, 0, 6]) + test_array("lp_var_sa_ub_up", [1, Inf, Inf, Inf, Inf]) + test_array("lp_var_sa_ub_down", [0, 0, 0, 1, 0]) + test_array("lp_var_sa_lb_up", [1, 0, 1, 1, 1]) + test_array("lp_var_sa_lb_down", [-Inf, -Inf, 0, -Inf, 0]) + lp_wallclock_time = h5.get_scalar("lp_wallclock_time") + @test lp_wallclock_time >= 0 + + model.optimize() + model.extract_after_mip(h5) + test_array("mip_constr_slacks", [1, 0, 0]) + test_array("mip_var_values", [1.0, 0.0, 0.0, 1.0, 0.0]) + test_scalar("mip_gap", 0) + test_scalar("mip_obj_bound", 11.0) + test_scalar("mip_obj_value", 11.0) + mip_wallclock_time = h5.get_scalar("mip_wallclock_time") + @test mip_wallclock_time >= 0 +end diff --git a/test/src/test_h5.jl b/test/src/test_h5.jl new file mode 100644 index 0000000..2b21ba7 --- /dev/null +++ b/test/src/test_h5.jl @@ -0,0 +1,37 @@ +using MIPLearn + +function test_h5() + h5 = H5File(tempname(), "w") + _test_roundtrip_scalar(h5, "A") + _test_roundtrip_scalar(h5, true) + _test_roundtrip_scalar(h5, 1) + _test_roundtrip_scalar(h5, 1.0) + @test h5.get_scalar("unknown-key") === nothing + _test_roundtrip_array(h5, [true, false]) + _test_roundtrip_array(h5, [1, 2, 3]) + _test_roundtrip_array(h5, [1.0, 2.0, 3.0]) + _test_roundtrip_str_array(h5, ["A", "BB", "CCC"]) + @test h5.get_array("unknown-key") === nothing + h5.close() +end + +function _test_roundtrip_scalar(h5, original) + h5.put_scalar("key", original) + recovered = h5.get_scalar("key") + @test recovered !== nothing + @test original == recovered +end + +function _test_roundtrip_array(h5, original) + h5.put_array("key", original) + recovered = h5.get_array("key") + @test recovered !== nothing + @test all(original .== recovered) +end + +function _test_roundtrip_str_array(h5, original) + h5.put_array("key", MIPLearn.to_str_array(original)) + recovered = MIPLearn.from_str_array(h5.get_array("key")) + @test recovered !== nothing + @test all(original .== recovered) +end