mirror of
https://github.com/ANL-CEEESA/MIPLearn.jl.git
synced 2025-12-06 00:18:51 -06:00
Replace tuples; make it work with plain JuMP models
This commit is contained in:
2
deps/build.jl
vendored
2
deps/build.jl
vendored
@@ -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.dev8`)
|
||||
run(`$pip install miplearn==0.2.0.dev9`)
|
||||
end
|
||||
|
||||
install_miplearn()
|
||||
|
||||
@@ -7,6 +7,7 @@ using JuMP
|
||||
|
||||
@pydef mutable struct PyJuMPInstance <: miplearn.Instance
|
||||
function __init__(self, model)
|
||||
init_miplearn_ext(model)
|
||||
self.model = model
|
||||
self.samples = []
|
||||
end
|
||||
@@ -22,25 +23,25 @@ using JuMP
|
||||
function get_variable_features(self, var_name)
|
||||
model = self.model
|
||||
v = variable_by_name(model, var_name)
|
||||
return model.ext[:miplearn][:variable_features][v]
|
||||
return get(model.ext[:miplearn][:variable_features], v, [0.0])
|
||||
end
|
||||
|
||||
function get_variable_category(self, var_name)
|
||||
model = self.model
|
||||
v = variable_by_name(model, var_name)
|
||||
return model.ext[:miplearn][:variable_categories][v]
|
||||
return get(model.ext[:miplearn][:variable_categories], v, "default")
|
||||
end
|
||||
|
||||
function get_constraint_features(self, cname)
|
||||
model = self.model
|
||||
c = constraint_by_name(model, cname)
|
||||
return model.ext[:miplearn][:constraint_features][c]
|
||||
return get(model.ext[:miplearn][:constraint_features], c, [0.0])
|
||||
end
|
||||
|
||||
function get_constraint_category(self, cname)
|
||||
model = self.model
|
||||
c = constraint_by_name(model, cname)
|
||||
return model.ext[:miplearn][:constraint_categories][c]
|
||||
return get(model.ext[:miplearn][:constraint_categories], c, "default")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +51,8 @@ struct JuMPInstance
|
||||
end
|
||||
|
||||
|
||||
function JuMPInstance(model::Model)
|
||||
function JuMPInstance(model)
|
||||
model isa Model || error("model should be a JuMP.Model. Found $(typeof(model)) instead.")
|
||||
return JuMPInstance(PyJuMPInstance(model))
|
||||
end
|
||||
|
||||
|
||||
@@ -316,49 +316,49 @@ function get_variables(
|
||||
values, rc = nothing, nothing
|
||||
|
||||
# Variable names
|
||||
names = Tuple(JuMP.name.(vars))
|
||||
names = JuMP.name.(vars)
|
||||
|
||||
# Primal values
|
||||
if !isempty(data.solution)
|
||||
values = Tuple([data.solution[v] for v in vars])
|
||||
values = [data.solution[v] for v in vars]
|
||||
end
|
||||
|
||||
if with_static
|
||||
# Lower bounds
|
||||
lb = Tuple(
|
||||
lb = [
|
||||
JuMP.is_binary(v) ? 0.0 :
|
||||
JuMP.has_lower_bound(v) ? JuMP.lower_bound(v) :
|
||||
-Inf
|
||||
for v in vars
|
||||
)
|
||||
]
|
||||
|
||||
# Upper bounds
|
||||
ub = Tuple(
|
||||
ub = [
|
||||
JuMP.is_binary(v) ? 1.0 :
|
||||
JuMP.has_upper_bound(v) ? JuMP.upper_bound(v) :
|
||||
Inf
|
||||
for v in vars
|
||||
)
|
||||
]
|
||||
|
||||
# Variable types
|
||||
types = Tuple(
|
||||
types = [
|
||||
JuMP.is_binary(v) ? "B" :
|
||||
JuMP.is_integer(v) ? "I" :
|
||||
"C"
|
||||
for v in vars
|
||||
)
|
||||
]
|
||||
|
||||
# Objective function coefficients
|
||||
obj = objective_function(data.model)
|
||||
obj_coeffs = Tuple(
|
||||
obj_coeffs = [
|
||||
v ∈ keys(obj.terms) ? obj.terms[v] : 0.0
|
||||
for v in vars
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
rc = isempty(data.reduced_costs) ? nothing : Tuple(data.reduced_costs)
|
||||
rc = isempty(data.reduced_costs) ? nothing : data.reduced_costs
|
||||
|
||||
return miplearn.features.VariableFeatures(
|
||||
vf = miplearn.features.VariableFeatures(
|
||||
names=names,
|
||||
lower_bounds=lb,
|
||||
upper_bounds=ub,
|
||||
@@ -367,6 +367,7 @@ function get_variables(
|
||||
reduced_costs=rc,
|
||||
values=values,
|
||||
)
|
||||
return vf
|
||||
end
|
||||
|
||||
|
||||
@@ -406,7 +407,7 @@ function get_constraints(
|
||||
if ftype == JuMP.AffExpr
|
||||
push!(
|
||||
lhs,
|
||||
Tuple(
|
||||
[
|
||||
(
|
||||
MOI.get(
|
||||
constr.model.moi_backend,
|
||||
@@ -420,7 +421,7 @@ function get_constraints(
|
||||
MOI.ConstraintFunction(),
|
||||
constr.index,
|
||||
).terms
|
||||
)
|
||||
]
|
||||
)
|
||||
if stype == MOI.EqualTo{Float64}
|
||||
push!(senses, "=")
|
||||
@@ -441,17 +442,12 @@ function get_constraints(
|
||||
end
|
||||
end
|
||||
|
||||
function to_tuple(x)
|
||||
x !== nothing || return nothing
|
||||
return Tuple(x)
|
||||
end
|
||||
|
||||
return miplearn.features.ConstraintFeatures(
|
||||
names=to_tuple(names),
|
||||
senses=to_tuple(senses),
|
||||
lhs=to_tuple(lhs),
|
||||
rhs=to_tuple(rhs),
|
||||
dual_values=to_tuple(dual_values),
|
||||
names=names,
|
||||
senses=senses,
|
||||
lhs=lhs,
|
||||
rhs=rhs,
|
||||
dual_values=dual_values,
|
||||
)
|
||||
end
|
||||
|
||||
@@ -471,23 +467,35 @@ end
|
||||
)
|
||||
end
|
||||
|
||||
add_constraints(self, cf) =
|
||||
function add_constraints(self, cf)
|
||||
lhs = cf.lhs
|
||||
if lhs isa Matrix
|
||||
# Undo incorrect automatic conversion performed by PyCall
|
||||
lhs = [col[:] for col in eachcol(lhs)]
|
||||
end
|
||||
add_constraints(
|
||||
self.data,
|
||||
lhs=[[term for term in constr] for constr in cf.lhs],
|
||||
rhs=[r for r in cf.rhs],
|
||||
senses=[s for s in cf.senses],
|
||||
names=[n for n in cf.names],
|
||||
lhs=lhs,
|
||||
rhs=cf.rhs,
|
||||
senses=cf.senses,
|
||||
names=cf.names,
|
||||
)
|
||||
end
|
||||
|
||||
are_constraints_satisfied(self, cf; tol=1e-5) =
|
||||
tuple(are_constraints_satisfied(
|
||||
function are_constraints_satisfied(self, cf; tol=1e-5)
|
||||
lhs = cf.lhs
|
||||
if lhs isa Matrix
|
||||
# Undo incorrect automatic conversion performed by PyCall
|
||||
lhs = [col[:] for col in eachcol(lhs)]
|
||||
end
|
||||
return are_constraints_satisfied(
|
||||
self.data,
|
||||
lhs=[[term for term in constr] for constr in cf.lhs],
|
||||
rhs=[r for r in cf.rhs],
|
||||
senses=[s for s in cf.senses],
|
||||
lhs=lhs,
|
||||
rhs=cf.rhs,
|
||||
senses=cf.senses,
|
||||
tol=tol,
|
||||
)...)
|
||||
)
|
||||
end
|
||||
|
||||
build_test_instance_infeasible(self) =
|
||||
build_test_instance_infeasible()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
function init_miplearn_ext(model)::Dict
|
||||
if :miplearn ∉ keys(model.ext)
|
||||
model.ext[:miplearn] = Dict{Symbol, Any}()
|
||||
model.ext[:miplearn][:instance_features] = [0.0]
|
||||
model.ext[:miplearn][:variable_features] = Dict{VariableRef, Vector{Float64}}()
|
||||
model.ext[:miplearn][:variable_categories] = Dict{VariableRef, String}()
|
||||
model.ext[:miplearn][:constraint_features] = Dict{ConstraintRef, Vector{Float64}}()
|
||||
|
||||
@@ -6,43 +6,55 @@ using JuMP
|
||||
using MIPLearn
|
||||
using Gurobi
|
||||
|
||||
@testset "macros" begin
|
||||
weights = [1.0, 2.0, 3.0]
|
||||
prices = [5.0, 6.0, 7.0]
|
||||
capacity = 3.0
|
||||
@testset "LearningSolver" begin
|
||||
@testset "model with annotations" begin
|
||||
# Create standard JuMP model
|
||||
weights = [1.0, 2.0, 3.0]
|
||||
prices = [5.0, 6.0, 7.0]
|
||||
capacity = 3.0
|
||||
model = Model()
|
||||
|
||||
# Create standard JuMP model
|
||||
model = Model()
|
||||
n = length(weights)
|
||||
@variable(model, x[1:n], Bin)
|
||||
@objective(model, Max, sum(x[i] * prices[i] for i in 1:n))
|
||||
@constraint(model, c1, sum(x[i] * weights[i] for i in 1:n) <= capacity)
|
||||
n = length(weights)
|
||||
@variable(model, x[1:n], Bin)
|
||||
@objective(model, Max, sum(x[i] * prices[i] for i in 1:n))
|
||||
@constraint(model, c1, sum(x[i] * weights[i] for i in 1:n) <= capacity)
|
||||
|
||||
# Add ML information to the model
|
||||
@feature(model, [5.0])
|
||||
@feature(c1, [1.0, 2.0, 3.0])
|
||||
@category(c1, "c1")
|
||||
for i in 1:n
|
||||
@feature(x[i], [weights[i]; prices[i]])
|
||||
@category(x[i], "type-$i")
|
||||
# Add ML information to the model
|
||||
@feature(model, [5.0])
|
||||
@feature(c1, [1.0, 2.0, 3.0])
|
||||
@category(c1, "c1")
|
||||
for i in 1:n
|
||||
@feature(x[i], [weights[i]; prices[i]])
|
||||
@category(x[i], "type-$i")
|
||||
end
|
||||
|
||||
# Should store ML information
|
||||
@test model.ext[:miplearn][:variable_features][x[1]] == [1.0, 5.0]
|
||||
@test model.ext[:miplearn][:variable_features][x[2]] == [2.0, 6.0]
|
||||
@test model.ext[:miplearn][:variable_features][x[3]] == [3.0, 7.0]
|
||||
@test model.ext[:miplearn][:variable_categories][x[1]] == "type-1"
|
||||
@test model.ext[:miplearn][:variable_categories][x[2]] == "type-2"
|
||||
@test model.ext[:miplearn][:variable_categories][x[3]] == "type-3"
|
||||
@test model.ext[:miplearn][:constraint_features][c1] == [1.0, 2.0, 3.0]
|
||||
@test model.ext[:miplearn][:constraint_categories][c1] == "c1"
|
||||
@test model.ext[:miplearn][:instance_features] == [5.0]
|
||||
|
||||
solver = LearningSolver(Gurobi.Optimizer)
|
||||
instance = JuMPInstance(model)
|
||||
stats = solve!(solver, instance)
|
||||
@test stats["mip_lower_bound"] == 11.0
|
||||
@test length(instance.py.samples) == 1
|
||||
fit!(solver, [instance])
|
||||
solve!(solver, instance)
|
||||
end
|
||||
|
||||
# Should store ML information
|
||||
@test model.ext[:miplearn][:variable_features][x[1]] == [1.0, 5.0]
|
||||
@test model.ext[:miplearn][:variable_features][x[2]] == [2.0, 6.0]
|
||||
@test model.ext[:miplearn][:variable_features][x[3]] == [3.0, 7.0]
|
||||
@test model.ext[:miplearn][:variable_categories][x[1]] == "type-1"
|
||||
@test model.ext[:miplearn][:variable_categories][x[2]] == "type-2"
|
||||
@test model.ext[:miplearn][:variable_categories][x[3]] == "type-3"
|
||||
@test model.ext[:miplearn][:constraint_features][c1] == [1.0, 2.0, 3.0]
|
||||
@test model.ext[:miplearn][:constraint_categories][c1] == "c1"
|
||||
@test model.ext[:miplearn][:instance_features] == [5.0]
|
||||
|
||||
solver = LearningSolver(Gurobi.Optimizer)
|
||||
instance = JuMPInstance(model)
|
||||
stats = solve!(solver, instance)
|
||||
@test stats["mip_lower_bound"] == 11.0
|
||||
@test length(instance.py.samples) == 1
|
||||
fit!(solver, [instance])
|
||||
solve!(solver, instance)
|
||||
@testset "plain model" begin
|
||||
model = Model()
|
||||
@variable(model, x, Bin)
|
||||
@variable(model, y, Bin)
|
||||
@objective(model, Max, x + y)
|
||||
solver = LearningSolver(Gurobi.Optimizer)
|
||||
instance = JuMPInstance(model)
|
||||
stats = solve!(solver, instance)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user