From 190c28820342eb009bb5b102c50b42b10b062ce0 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Thu, 1 Feb 2024 16:56:45 -0600 Subject: [PATCH] Make lazy constraints compatible with JuMP --- src/MIPLearn.jl | 2 + src/problems/tsp.jl | 71 +++++++++++++++++++++++++++++ src/solvers/jump.jl | 36 +++++++++++++-- test/Project.toml | 1 + test/fixtures/tsp-n20-00000.h5 | Bin 0 -> 62978 bytes test/fixtures/tsp-n20-00000.pkl.gz | Bin 0 -> 1145 bytes test/src/MIPLearnT.jl | 3 ++ test/src/components/test_cuts.jl | 2 +- test/src/components/test_lazy.jl | 46 +++++++++++++++++++ test/src/problems/test_tsp.jl | 27 +++++++++++ test/src/test_usage.jl | 14 +++--- 11 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 src/problems/tsp.jl create mode 100644 test/fixtures/tsp-n20-00000.h5 create mode 100644 test/fixtures/tsp-n20-00000.pkl.gz create mode 100644 test/src/components/test_lazy.jl create mode 100644 test/src/problems/test_tsp.jl diff --git a/src/MIPLearn.jl b/src/MIPLearn.jl index ed670ac..5034474 100644 --- a/src/MIPLearn.jl +++ b/src/MIPLearn.jl @@ -13,6 +13,7 @@ include("extractors.jl") include("io.jl") include("problems/setcover.jl") include("problems/stab.jl") +include("problems/tsp.jl") include("solvers/jump.jl") include("solvers/learning.jl") @@ -23,6 +24,7 @@ function __init__() __init_io__() __init_problems_setcover__() __init_problems_stab__() + __init_problems_tsp__() __init_solvers_jump__() __init_solvers_learning__() end diff --git a/src/problems/tsp.jl b/src/problems/tsp.jl new file mode 100644 index 0000000..3f86b5b --- /dev/null +++ b/src/problems/tsp.jl @@ -0,0 +1,71 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2024, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using JuMP + +global TravelingSalesmanData = PyNULL() +global TravelingSalesmanGenerator = PyNULL() + +function __init_problems_tsp__() + copy!(TravelingSalesmanData, pyimport("miplearn.problems.tsp").TravelingSalesmanData) + copy!(TravelingSalesmanGenerator, pyimport("miplearn.problems.tsp").TravelingSalesmanGenerator) +end + +function build_tsp_model_jump(data::Any; optimizer=HiGHS.Optimizer) + nx = pyimport("networkx") + + if data isa String + data = read_pkl_gz(data) + end + model = Model(optimizer) + edges = [(i, j) for i in 1:data.n_cities for j in (i+1):data.n_cities] + x = @variable(model, x[edges], Bin) + @objective(model, Min, sum( + x[(i, j)] * data.distances[i, j] for (i, j) in edges + )) + + # Eq: Must choose two edges adjacent to each node + @constraint( + model, + eq_degree[i in 1:data.n_cities], + sum(x[(min(i, j), max(i, j))] for j in 1:data.n_cities if i != j) == 2 + ) + + function lazy_separate(cb_data) + x_val = callback_value.(Ref(cb_data), x) + violations = [] + selected_edges = [e for e in edges if x_val[e] > 0.5] + graph = nx.Graph() + graph.add_edges_from(selected_edges) + for component in nx.connected_components(graph) + if length(component) < data.n_cities + cut_edges = [ + [e[1], e[2]] + for e in edges + if (e[1] ∈ component && e[2] ∉ component) + || + (e[1] ∉ component && e[2] ∈ component) + ] + push!(violations, cut_edges) + end + end + return violations + end + + function lazy_enforce(violations) + @info "Adding $(length(violations)) subtour elimination eqs..." + for violation in violations + constr = @build_constraint(sum(x[(e[1], e[2])] for e in violation) >= 2) + submit(model, constr) + end + end + + return JumpModel( + model, + lazy_enforce=lazy_enforce, + lazy_separate=lazy_separate, + ) +end + +export TravelingSalesmanData, TravelingSalesmanGenerator, build_tsp_model_jump diff --git a/src/solvers/jump.jl b/src/solvers/jump.jl index 9a84d02..6c45a4e 100644 --- a/src/solvers/jump.jl +++ b/src/solvers/jump.jl @@ -12,9 +12,12 @@ Base.@kwdef mutable struct _JumpModelExtData aot_cuts = nothing cb_data = nothing cuts = [] + lazy = [] where::Symbol = :WHERE_DEFAULT cuts_enforce::Union{Function,Nothing} = nothing cuts_separate::Union{Function,Nothing} = nothing + lazy_enforce::Union{Function,Nothing} = nothing + lazy_separate::Union{Function,Nothing} = nothing end function JuMP.copy_extension_data( @@ -58,8 +61,10 @@ function submit(model::JuMP.Model, constr) ext = model.ext[:miplearn] if ext.where == :WHERE_CUTS MOI.submit(model, MOI.UserCut(ext.cb_data), constr) + elseif ext.where == :WHERE_LAZY + MOI.submit(model, MOI.LazyConstraint(ext.cb_data), constr) else - error("not implemented") + add_constraint(model, constr) end end @@ -281,9 +286,10 @@ function _extract_after_mip(model::JuMP.Model, h5) slacks = abs.(lhs * x - rhs) h5.put_array("mip_constr_slacks", slacks) - # Cuts + # Cuts and lazy constraints ext = model.ext[:miplearn] h5.put_scalar("mip_cuts", JSON.json(ext.cuts)) + h5.put_scalar("mip_lazy", JSON.json(ext.lazy)) end function _fix_variables(model::JuMP.Model, var_names, var_values, stats) @@ -318,6 +324,23 @@ function _optimize(model::JuMP.Model) set_attribute(model, MOI.UserCutCallback(), cut_callback) end + # Set up lazy constraint callbacks + ext.lazy = [] + function lazy_callback(cb_data) + ext.cb_data = cb_data + ext.where = :WHERE_LAZY + violations = ext.lazy_separate(cb_data) + for v in violations + push!(ext.lazy, v) + end + if !isempty(violations) + ext.lazy_enforce(violations) + end + end + if ext.lazy_separate !== nothing + set_attribute(model, MOI.LazyConstraintCallback(), lazy_callback) + end + # Optimize ext.where = :WHERE_DEFAULT optimize!(model) @@ -363,12 +386,15 @@ function __init_solvers_jump__() inner; cuts_enforce::Union{Function,Nothing}=nothing, cuts_separate::Union{Function,Nothing}=nothing, + lazy_enforce::Union{Function,Nothing}=nothing, + lazy_separate::Union{Function,Nothing}=nothing, ) - AbstractModel.__init__(self) self.inner = inner self.inner.ext[:miplearn] = _JumpModelExtData( cuts_enforce=cuts_enforce, cuts_separate=cuts_separate, + lazy_enforce=lazy_enforce, + lazy_separate=lazy_separate, ) end @@ -409,6 +435,10 @@ function __init_solvers_jump__() function set_cuts(self, cuts) self.inner.ext[:miplearn].aot_cuts = cuts end + + function lazy_enforce(self, model, violations) + self.inner.ext[:miplearn].lazy_enforce(violations) + end end copy!(JumpModel, Class) end diff --git a/test/Project.toml b/test/Project.toml index 0854405..e5abcbf 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,6 +5,7 @@ version = "0.1.0" [deps] Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d" +GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" diff --git a/test/fixtures/tsp-n20-00000.h5 b/test/fixtures/tsp-n20-00000.h5 new file mode 100644 index 0000000000000000000000000000000000000000..2539134913d20664f0b3cc8caacd9ea7ee0acd1a GIT binary patch literal 62978 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 literal 0 HcmV?d00001 diff --git a/test/src/MIPLearnT.jl b/test/src/MIPLearnT.jl index e3524bb..df7799f 100644 --- a/test/src/MIPLearnT.jl +++ b/test/src/MIPLearnT.jl @@ -17,9 +17,11 @@ include("fixtures.jl") include("BB/test_bb.jl") include("components/test_cuts.jl") +include("components/test_lazy.jl") include("Cuts/BlackBox/test_cplex.jl") include("problems/test_setcover.jl") include("problems/test_stab.jl") +include("problems/test_tsp.jl") include("solvers/test_jump.jl") include("test_io.jl") include("test_usage.jl") @@ -32,6 +34,7 @@ function runtests() test_io() test_problems_setcover() test_problems_stab() + test_problems_tsp() test_solvers_jump() test_usage() test_cuts() diff --git a/test/src/components/test_cuts.jl b/test/src/components/test_cuts.jl index f466732..f9cccd9 100644 --- a/test/src/components/test_cuts.jl +++ b/test/src/components/test_cuts.jl @@ -27,7 +27,7 @@ function gen_stab() end function test_cuts() - data_filenames = ["$BASEDIR/../fixtures/stab-n50-0000$i.pkl.gz" for i in 0:0] + data_filenames = ["$BASEDIR/../fixtures/stab-n50-00000.pkl.gz"] clf = pyimport("sklearn.dummy").DummyClassifier() extractor = H5FieldsExtractor( instance_fields=["static_var_obj_coeffs"], diff --git a/test/src/components/test_lazy.jl b/test/src/components/test_lazy.jl new file mode 100644 index 0000000..291b5fa --- /dev/null +++ b/test/src/components/test_lazy.jl @@ -0,0 +1,46 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2024, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using GLPK + +function gen_tsp() + np = pyimport("numpy") + uniform = pyimport("scipy.stats").uniform + randint = pyimport("scipy.stats").randint + np.random.seed(42) + + gen = TravelingSalesmanGenerator( + x=uniform(loc=0.0, scale=1000.0), + y=uniform(loc=0.0, scale=1000.0), + n=randint(low=20, high=21), + gamma=uniform(loc=1.0, scale=0.25), + fix_cities=true, + round=true, + ) + data = gen.generate(1) + data_filenames = write_pkl_gz(data, "$BASEDIR/../fixtures", prefix="tsp-n20-") + collector = BasicCollector(write_mps=false) + collector.collect( + data_filenames, + data -> build_tsp_model_jump(data, optimizer=GLPK.Optimizer), + progress=true, + verbose=true, + ) +end + +function test_lazy() + data_filenames = ["$BASEDIR/../fixtures/tsp-n20-00000.pkl.gz"] + clf = pyimport("sklearn.dummy").DummyClassifier() + extractor = H5FieldsExtractor( + instance_fields=["static_var_obj_coeffs"], + ) + comp = MemorizingLazyComponent(clf=clf, extractor=extractor) + solver = LearningSolver(components=[comp]) + solver.fit(data_filenames) + stats = solver.optimize( + data_filenames[1], + data -> build_tsp_model_jump(data, optimizer=GLPK.Optimizer), + ) + @test stats["Lazy Constraints: AOT"] > 0 +end diff --git a/test/src/problems/test_tsp.jl b/test/src/problems/test_tsp.jl new file mode 100644 index 0000000..b56c01f --- /dev/null +++ b/test/src/problems/test_tsp.jl @@ -0,0 +1,27 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2024, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +using GLPK +using JuMP + +function test_problems_tsp() + pdist = pyimport("scipy.spatial.distance").pdist + squareform = pyimport("scipy.spatial.distance").squareform + + data = TravelingSalesmanData( + n_cities=6, + distances=squareform(pdist([ + [0.0, 0.0], + [1.0, 0.0], + [2.0, 0.0], + [3.0, 0.0], + [0.0, 1.0], + [3.0, 1.0], + ])), + ) + model = build_tsp_model_jump(data, optimizer=GLPK.Optimizer) + model.optimize() + @test objective_value(model.inner) == 8.0 + return +end diff --git a/test/src/test_usage.jl b/test/src/test_usage.jl index 9a8cb7b..3e41332 100644 --- a/test/src/test_usage.jl +++ b/test/src/test_usage.jl @@ -13,22 +13,22 @@ function test_usage() @debug "Setting up LearningSolver..." solver = LearningSolver( - components = [ + components=[ IndependentVarsPrimalComponent( - base_clf = SingleClassFix( + base_clf=SingleClassFix( MinProbabilityClassifier( - base_clf = LogisticRegression(), - thresholds = [0.95, 0.95], + base_clf=LogisticRegression(), + thresholds=[0.95, 0.95], ), ), - extractor = AlvLouWeh2017Extractor(), - action = SetWarmStart(), + extractor=AlvLouWeh2017Extractor(), + action=SetWarmStart(), ), ], ) @debug "Collecting training data..." - bc = BasicCollector() + bc = BasicCollector(write_mps=false) bc.collect(data_filenames, build_setcover_model_jump) @debug "Training models..."