Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managed Bounded LMO. #149

Merged
merged 17 commits into from
Oct 25, 2023
1 change: 0 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
MathOptSetDistances = "3b969827-a86c-476c-9527-bb6f1a8fbad5"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[weakdeps]
Expand Down
82 changes: 79 additions & 3 deletions examples/approx_planted_point.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,96 @@ diffi = Random.rand(Bool, n) * 0.6 .+ 0.3
end

@testset "Using Cube LMO" begin
int_vars = []
bin_vars = collect(1:n)
int_vars = collect(1:n)

bounds = Boscia.IntegerBounds()
for i in 1:n
push!(bounds, (i, 0.0), :greaterthan)
push!(bounds, (i, 1.0), :lessthan)
end
blmo = Boscia.CubeBLMO(n, int_vars, bin_vars, bounds)
blmo = Boscia.CubeBLMO(n, int_vars, bounds)

x, _, result = Boscia.solve(f, grad!, blmo, verbose =true)

@test x == round.(diffi)
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end

@testset "Using Cube Simple LMO" begin
int_vars = collect(1:n)
lbs = zeros(n)
ubs = ones(n)

sblmo = Boscia.CubeSimBLMO(lbs, ubs, int_vars)

x, _, result = Boscia.solve(f, grad!, sblmo, lbs[int_vars], ubs[int_vars], int_vars, n, verbose =true)

@test x == round.(diffi)
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end
end


@testset "Approximate planted point - Mixed" begin

function f(x)
return 0.5 * sum((x[i] - diffi[i])^2 for i in eachindex(x))
end
function grad!(storage, x)
@. storage = x - diffi
end

int_vars = unique!(rand(collect(1:n), Int(floor(n/2))))

@testset "Using SCIP" begin
o = SCIP.Optimizer()
MOI.set(o, MOI.Silent(), true)
MOI.empty!(o)
x = MOI.add_variables(o, n)
for xi in x
MOI.add_constraint(o, xi, MOI.GreaterThan(0.0))
MOI.add_constraint(o, xi, MOI.LessThan(1.0))
if xi.value in int_vars
MOI.add_constraint(o, xi, MOI.ZeroOne()) # or MOI.Integer()
end
end
lmo = FrankWolfe.MathOptLMO(o)

x, _, result = Boscia.solve(f, grad!, lmo, verbose=true)

sol = diffi
sol[int_vars] = round.(sol[int_vars])
@test sum(isapprox.(x, sol, atol =1e-6, rtol=1e-2)) == n
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end

@testset "Using Cube LMO" begin
bounds = Boscia.IntegerBounds()
for i in 1:n
push!(bounds, (i, 0.0), :greaterthan)
push!(bounds, (i, 1.0), :lessthan)
end
blmo = Boscia.CubeBLMO(n, int_vars, bounds)

x, _, result = Boscia.solve(f, grad!, blmo, verbose =true)

sol = diffi
sol[int_vars] = round.(sol[int_vars])
@test sum(isapprox.(x, sol, atol =1e-6, rtol=1e-2)) == n
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end

@testset "Using Cube Simple LMO" begin
lbs = zeros(n)
ubs = ones(n)

sblmo = Boscia.CubeSimBLMO(lbs, ubs, int_vars)

x, _, result = Boscia.solve(f, grad!, sblmo, lbs[int_vars], ubs[int_vars], int_vars, n, verbose =true)

sol = diffi
sol[int_vars] = round.(sol[int_vars])
@test sum(isapprox.(x, sol, atol =1e-6, rtol=1e-2)) == n
@test isapprox(f(x), f(result[:raw_solution]), atol=1e-6, rtol=1e-3)
end
end
2 changes: 2 additions & 0 deletions src/Boscia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ using MathOptInterface
const MOI = MathOptInterface
const MOIU = MOI.Utilities
import MathOptSetDistances as MOD

include("integer_bounds.jl")
include("blmo_interface.jl")
include("time_tracking_lmo.jl")
Expand All @@ -24,6 +25,7 @@ include("heuristics.jl")
include("strong_branching.jl")
include("utilities.jl")
include("interface.jl")
include("managed_blmo.jl")
include("MOI_bounded_oracle.jl")
include("cube_blmo.jl")

Expand Down
54 changes: 29 additions & 25 deletions src/MOI_bounded_oracle.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ end
"""
Get list of binary and integer variables, respectively.
"""
function Boscia.get_binary_variables(blmo::MathOptBLMO)
function get_binary_variables(blmo::MathOptBLMO)
return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}())
end
function Boscia.get_integer_variables(blmo::MathOptBLMO)
return MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}())
bin_var = get_binary_variables(blmo)
int_var = MOI.get(blmo.o, MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.Integer}())
return vcat(getproperty.(int_var, :value), getproperty.(bin_var, :value))
end

"""
Expand Down Expand Up @@ -123,7 +125,7 @@ end
Delete bounds.
"""
function Boscia.delete_bounds!(blmo::MathOptBLMO, cons_delete)
for d_idx in cons_delete
for (d_idx, _) in cons_delete
MOI.delete(blmo.o, d_idx)
end
end
Expand All @@ -142,7 +144,7 @@ end
"""
Has variable a binary constraint?
"""
function Boscia.has_binary_constraint(blmo::MathOptBLMO, idx::Int)
function has_binary_constraint(blmo::MathOptBLMO, idx::Int)
consB_list = MOI.get(
blmo.o,
MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}(),
Expand Down Expand Up @@ -210,6 +212,27 @@ function is_linear_feasible_subroutine(o::MOI.ModelLike, ::Type{F}, ::Type{S}, v
return true
end

"""
Add explicit bounds for binary variables.
"""
function explicit_bounds_binary_var(blmo::MathOptBLMO, global_bounds::Boscia.IntegerBounds)
# adding binary bounds explicitly
binary_variables = get_binary_variables(blmo)
for idx in binary_variables
cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(idx.value)
if !MOI.is_valid(blmo.o, cidx)
MOI.add_constraint(blmo.o, MOI.VariableIndex(idx.value), MOI.LessThan(1.0))
end
@assert MOI.is_valid(blmo.o, cidx)
cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(idx.value)
if !MOI.is_valid(blmo.o, cidx)
MOI.add_constraint(blmo.o, MOI.VariableIndex(idx.value), MOI.GreaterThan(0.0))
end
global_bounds[idx.value, :greaterthan] = 0.0
global_bounds[idx.value, :lessthan] = 1.0
end
end

"""
Read global bounds from the problem
"""
Expand Down Expand Up @@ -240,29 +263,10 @@ function Boscia.build_global_bounds(blmo::MathOptBLMO, integer_variables)
end
@assert !MOI.is_valid(blmo.o, cidx)
end
explicit_bounds_binary_var(blmo, global_bounds)
return global_bounds
end

"""
Add explicit bounds for binary variables.
"""
function Boscia.explicit_bounds_binary_var(blmo::MathOptBLMO, global_bounds::Boscia.IntegerBounds, binary_variables)
# adding binary bounds explicitly
for idx in binary_variables
cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{Float64}}(idx)
if !MOI.is_valid(blmo.o, cidx)
MOI.add_constraint(blmo.o, MOI.VariableIndex(idx), MOI.LessThan(1.0))
end
@assert MOI.is_valid(blmo.o, cidx)
cidx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(idx)
if !MOI.is_valid(blmo.o, cidx)
MOI.add_constraint(blmo.o, MOI.VariableIndex(idx), MOI.GreaterThan(0.0))
end
global_bounds[idx, :greaterthan] = 0.0
global_bounds[idx, :lessthan] = 1.0
end
end


##################### Optional to implement ################

Expand Down Expand Up @@ -316,7 +320,7 @@ end
Check whether a split is valid, i.e. the upper and lower on variable vidx are not the same.
"""
function Boscia.is_valid_split(tree::Bonobo.BnBTree, blmo::MathOptBLMO, vidx::Int)
bin_var, _ = Boscia.has_binary_constraint(blmo, vidx)
bin_var, _ = has_binary_constraint(blmo, vidx)
int_var, _ = Boscia.has_integer_constraint(blmo, vidx)
matbesancon marked this conversation as resolved.
Show resolved Hide resolved
if int_var || bin_var
l_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.GreaterThan{Float64}}(vidx)
Expand Down
16 changes: 0 additions & 16 deletions src/blmo_interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,6 @@ Read global bounds from the problem.
"""
function build_global_bounds end

"""
Add explicit bounds for binary variables, if not already done from the get-go.
"""
function explicit_bounds_binary_var end


## Read information from problem

"""
Expand All @@ -44,11 +38,6 @@ If the problem has n variables, they are expected to contiguous and ordered from
"""
function get_list_of_variables end

"""
Get list of binary variables.
"""
function get_binary_variables end

"""
Get list of integer variables.
"""
Expand Down Expand Up @@ -109,11 +98,6 @@ That means does v satisfy all bounds and other linear constraints?
"""
function is_linear_feasible end

"""
Has variable a binary constraint?
"""
function has_binary_constraint end

"""
Has variable an integer constraint?
"""
Expand Down
4 changes: 2 additions & 2 deletions src/build_lmo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function build_LMO(
end
else
# Delete
push!(cons_delete, c_idx)
push!(cons_delete, (c_idx, :greaterthan))
end
end
end
Expand All @@ -53,7 +53,7 @@ function build_LMO(
end
else
# Delete
push!(cons_delete, c_idx)
push!(cons_delete, (c_idx, :lessthan))
end
end
end
Expand Down
53 changes: 38 additions & 15 deletions src/cube_blmo.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ A Bounded Linear Minimization Oracle over a cube.
mutable struct CubeBLMO <: BoundedLinearMinimizationOracle
n::Int
int_vars::Vector{Int}
bin_vars::Vector{Int}
bounds::IntegerBounds
solving_time::Float64
end

CubeBLMO(n, int_vars, bin_vars, bounds) = CubeBLMO(n, int_vars, bin_vars, bounds, 0.0)
CubeBLMO(n, int_vars, bounds) = CubeBLMO(n, int_vars, bounds, 0.0)

## Necessary

Expand Down Expand Up @@ -40,20 +39,13 @@ function build_global_bounds(blmo::CubeBLMO, integer_variables)
return global_bounds
end

function explicit_bounds_binary_var(blmo::CubeBLMO, gb::IntegerBounds, binary_vars)
nothing
end

## Read information from problem
function get_list_of_variables(blmo::CubeBLMO)
return blmo.n, collect(1:blmo.n)
end

# Get list of binary and integer variables, respectively.
function get_binary_variables(blmo::CubeBLMO)
return blmo.bin_vars
end

# Get list of integer variables, respectively.
function get_integer_variables(blmo::CubeBLMO)
return blmo.int_vars
end
Expand Down Expand Up @@ -126,12 +118,8 @@ function is_linear_feasible(blmo::CubeBLMO, v::AbstractVector)
return true
end

function has_binary_constraint(blmo::CubeBLMO, idx)
return idx in blmo.int_vars
end

function has_integer_constraint(blmo::CubeBLMO, idx)
return idx in blmo.bin_vars
return idx in blmo.int_vars
end


Expand Down Expand Up @@ -173,3 +161,38 @@ end
function get_BLMO_solve_data(blmo::CubeBLMO)
return blmo.solving_time, 0.0, 0.0
end

########################################################################
"""
CubeSimpleBLMO{T}(lower_bounds, upper_bounds)

Hypercube with lower and upper bounds implementing the `SimpleBoundableLMO` interface.
"""
struct CubeSimBLMO <: SimpleBoundableLMO
lower_bounds::Vector{Float64}
upper_bounds::Vector{Float64}
int_vars::Vector{Int}
end

function bounded_compute_extreme_point(sblmo::CubeSimBLMO, d, lb, ub, int_vars; kwargs...)
v = zeros(length(d))
for i in eachindex(d)
if i in int_vars
idx = findfirst(x -> x == i, int_vars)
v[i] = d[i] > 0 ? lb[idx] : ub[idx]
else
v[i] = d[i] > 0 ? sblmo.lower_bounds[i] : sblmo.upper_bounds[i]
end
end
return v
end

function is_linear_feasible(sblmo::CubeSimBLMO, v)
for i in setdiff(eachindex(v), sblmo.int_vars)
if !(sblmo.lower_bounds[i] ≤ v[i] + 1e-6 || !(v[i] - 1e-6 ≤ blmo.upper_bounds[i]))
@debug("Vertex entry: $(v[i]) Lower bound: $(blmo.bounds[i, :greaterthan]) Upper bound: $(blmo.bounds[i, :lessthan]))")
return false
end
end
return true
end
Loading
Loading