From 2ea0043c03a2ea36c307f9cbce4aaaf53f90d0fd Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 11 Jun 2025 15:38:06 -0500 Subject: [PATCH] Add support for MIQPs; implement max cut model --- deps/build.jl | 2 +- src/MIPLearn.jl | 2 ++ src/problems/maxcut.jl | 31 ++++++++++++++++++ src/solvers/jump.jl | 31 +++++++++++++----- test/src/MIPLearnT.jl | 2 ++ test/src/problems/test_maxcut.jl | 54 ++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 src/problems/maxcut.jl create mode 100644 test/src/problems/test_maxcut.jl diff --git a/deps/build.jl b/deps/build.jl index 10aa0a3..f5188dd 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.4.2`) + run(`$pip install miplearn==0.4.4`) end install_miplearn() diff --git a/src/MIPLearn.jl b/src/MIPLearn.jl index 8ce7cad..52c2940 100644 --- a/src/MIPLearn.jl +++ b/src/MIPLearn.jl @@ -13,6 +13,7 @@ include("collectors.jl") include("components.jl") include("extractors.jl") include("io.jl") +include("problems/maxcut.jl") include("problems/setcover.jl") include("problems/stab.jl") include("problems/tsp.jl") @@ -24,6 +25,7 @@ function __init__() __init_components__() __init_extractors__() __init_io__() + __init_problems_maxcut__() __init_problems_setcover__() __init_problems_stab__() __init_problems_tsp__() diff --git a/src/problems/maxcut.jl b/src/problems/maxcut.jl new file mode 100644 index 0000000..4bcd5fb --- /dev/null +++ b/src/problems/maxcut.jl @@ -0,0 +1,31 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using JuMP + +global MaxCutData = PyNULL() +global MaxCutGenerator = PyNULL() + +function __init_problems_maxcut__() + copy!(MaxCutData, pyimport("miplearn.problems.maxcut").MaxCutData) + copy!(MaxCutGenerator, pyimport("miplearn.problems.maxcut").MaxCutGenerator) +end + +function build_maxcut_model_jump(data::Any; optimizer) + if data isa String + data = read_pkl_gz(data) + end + nodes = collect(data.graph.nodes()) + edges = collect(data.graph.edges()) + model = Model(optimizer) + @variable(model, x[nodes], Bin) + @objective( + model, + Min, + sum(-data.weights[i] * x[e[1]] * (1 - x[e[2]]) for (i, e) in enumerate(edges)) + ) + return JumpModel(model) +end + +export MaxCutData, MaxCutGenerator, build_maxcut_model_jump diff --git a/src/solvers/jump.jl b/src/solvers/jump.jl index a8ddd15..6ac185f 100644 --- a/src/solvers/jump.jl +++ b/src/solvers/jump.jl @@ -89,14 +89,27 @@ function _extract_after_load_vars(model::JuMP.Model, h5) 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] + + # Linear obj terms + obj = objective_function(model, QuadExpr) + obj_coeffs_linear = [v ∈ keys(obj.aff.terms) ? obj.aff.terms[v] : 0.0 for v in vars] + + # Quadratic obj terms + if length(obj) > 0 + nvars = length(vars) + obj_coeffs_quad = zeros(nvars, nvars) + for (pair, coeff) in obj.terms + obj_coeffs_quad[pair.a.index.value, pair.b.index.value] = coeff + end + h5.put_array("static_var_obj_coeffs_quad", obj_coeffs_quad) + end + 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) + h5.put_array("static_var_obj_coeffs", obj_coeffs_linear) + h5.put_scalar("static_obj_offset", obj.aff.constant) end function _extract_after_load_constrs(model::JuMP.Model, h5) @@ -143,7 +156,7 @@ function _extract_after_load_constrs(model::JuMP.Model, h5) end end if isempty(names) - error("no model constraints found; note that MIPLearn ignores unnamed constraints") + return end lhs = sparse(lhs_rows, lhs_cols, lhs_values, length(rhs), JuMP.num_variables(model)) h5.put_sparse("static_constr_lhs", lhs) @@ -282,9 +295,11 @@ function _extract_after_mip(model::JuMP.Model, h5) # 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) + if lhs !== nothing + rhs = h5.get_array("static_constr_rhs") + slacks = abs.(lhs * x - rhs) + h5.put_array("mip_constr_slacks", slacks) + end # Cuts and lazy constraints ext = model.ext[:miplearn] diff --git a/test/src/MIPLearnT.jl b/test/src/MIPLearnT.jl index e52e920..d5aa6ff 100644 --- a/test/src/MIPLearnT.jl +++ b/test/src/MIPLearnT.jl @@ -24,6 +24,7 @@ include("Cuts/tableau/test_gmi_dual.jl") include("problems/test_setcover.jl") include("problems/test_stab.jl") include("problems/test_tsp.jl") +include("problems/test_maxcut.jl") include("solvers/test_jump.jl") include("test_io.jl") include("test_usage.jl") @@ -37,6 +38,7 @@ function runtests() test_problems_setcover() test_problems_stab() test_problems_tsp() + test_problems_maxcut() test_solvers_jump() test_usage() test_cuts() diff --git a/test/src/problems/test_maxcut.jl b/test/src/problems/test_maxcut.jl new file mode 100644 index 0000000..f4d8695 --- /dev/null +++ b/test/src/problems/test_maxcut.jl @@ -0,0 +1,54 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using PyCall + +function test_problems_maxcut() + np = pyimport("numpy") + random = pyimport("random") + scipy_stats = pyimport("scipy.stats") + randint = scipy_stats.randint + uniform = scipy_stats.uniform + + # Set random seed + random.seed(42) + np.random.seed(42) + + # Build random instance + data = MaxCutGenerator( + n = randint(low = 10, high = 11), + p = uniform(loc = 0.5, scale = 0.0), + fix_graph = false, + ).generate( + 1, + )[1] + + # Build model + model = build_maxcut_model_jump(data, optimizer = SCIP.Optimizer) + + # Check static features + h5 = H5File(tempname(), "w") + model.extract_after_load(h5) + obj_linear = h5.get_array("static_var_obj_coeffs") + obj_quad = h5.get_array("static_var_obj_coeffs_quad") + @test obj_linear == [3.0, 1.0, 3.0, 1.0, -1.0, 0.0, -1.0, 0.0, -1.0, 0.0] + @test obj_quad == [ + 0.0 0.0 -1.0 1.0 -1.0 0.0 0.0 0.0 -1.0 -1.0 + 0.0 0.0 1.0 -1.0 0.0 -1.0 -1.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 -1.0 -1.0 + 0.0 0.0 0.0 0.0 0.0 -1.0 1.0 -1.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + ] + + # Check optimal solution + model.optimize() + model.extract_after_mip(h5) + @test h5.get_scalar("mip_obj_value") == -4 + h5.close() +end