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

Consider variable bounds clean #191

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
46 changes: 42 additions & 4 deletions src/jump.jl
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@ function JuMP.optimize!(
bilevel_prob = "",
solver_prob = "",
file_format = MOI.FileFormats.FORMAT_AUTOMATIC,
consider_constrained_variables = false,
)
if model.mode === nothing
error(
Expand Down Expand Up @@ -581,6 +582,7 @@ function JuMP.optimize!(
moi_link2;
copy_names = model.copy_names,
pass_start = model.pass_start,
consider_constrained_variables = consider_constrained_variables,
)

# pass additional info (hints - not actual problem data)
Expand All @@ -590,10 +592,14 @@ function JuMP.optimize!(
ctr = model.ctr_lower[idx]
# this fails for vector-constrained variables due dualization 0.3.5
# because of constrained variables that change the dual
pre_duals =
lower_primal_dual_map.primal_con_dual_var[JuMP.index(ctr)] # vector
duals = map(x -> lower_dual_to_sblm[x], pre_duals)
pass_dual_info(single_blm, duals, info)
pass_necessary_dual_info(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why a function if a simple conditional would suffice?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, see above

single_blm,
info,
consider_constrained_variables,
lower_primal_dual_map,
lower_dual_to_sblm,
JuMP.index(ctr),
)
end
end
# pass lower & upper level primal variables info (upper, lower)
Expand Down Expand Up @@ -681,6 +687,38 @@ function pass_primal_info(single_blm, primal, info::BilevelVariableInfo)
return
end

function pass_necessary_dual_info(
single_blm,
info,
consider_constrained_variables::Bool,
lower_primal_dual_map,
lower_dual_to_sblm,
ctr_idx::MOI.ConstraintIndex{F,S},
) where {F<:Union{MOI.VariableIndex,MOI.VectorOfVariables},S}
if consider_constrained_variables
return
else
pre_duals = lower_primal_dual_map.primal_con_dual_var[ctr_idx] # vector
duals = map(x -> lower_dual_to_sblm[x], pre_duals)
pass_dual_info(single_blm, duals, info)
end
return
end

function pass_necessary_dual_info(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same function twice?

Copy link
Author

@LukasBarner LukasBarner Oct 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, thought I already replied to these when I made the commits ...
now a conditional

single_blm,
info,
consider_constrained_variables::Bool,
lower_primal_dual_map,
lower_dual_to_sblm,
ctr_idx::MOI.ConstraintIndex{F,S},
) where {F,S}
pre_duals = lower_primal_dual_map.primal_con_dual_var[ctr_idx] # vector
duals = map(x -> lower_dual_to_sblm[x], pre_duals)
pass_dual_info(single_blm, duals, info)
return
end

function pass_dual_info(single_blm, dual, info::BilevelConstraintInfo{Float64})
if !isnan(info.start)
MOI.set(single_blm, MOI.VariablePrimalStart(), dual[], info.start)
Expand Down
109 changes: 108 additions & 1 deletion src/moi.jl
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,88 @@ end
accept_vector_set(::ProductMode{T}, ::Complement) where {T} = nothing
accept_vector_set(::MixedMode{T}, ::Complement) where {T} = nothing

function get_variable_complement(primal_model, dual_model, primal_con, dual_con)
return error(
"An internal error with variable complements occurred. Likely, your problem type does not yet support consideration of constrained variables.",
)
end

function get_variable_complement(
primal_model,
dual_model,
primal_con::MOI.ConstraintIndex{Fp,Sp},
dual_con::MOI.ConstraintIndex{Fd,Sd},
) where {
Fp<:MOI.VariableIndex,
Sp<:Union{MOI.LessThan{T},MOI.GreaterThan{T}},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCALAR_SETS ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer this to throw an error in case of equalities (i.e. if something unexpected happens).
Could also go with SCALAR_SETS for code readability in case you prefer and do @Assert inside the function?

Fd,
Sd<:Union{MOI.LessThan{T},MOI.GreaterThan{T}},
} where {T}
primal_variable =
MOI.get(primal_model, MOI.ConstraintFunction(), primal_con)
primal_set = MOI.get(primal_model, MOI.ConstraintSet(), primal_con)

@assert MOI.constant(primal_set) == 0 "Unexpected variable bound"

dual_func = MOI.get(dual_model, MOI.ConstraintFunction(), dual_con)
dual_set = MOI.get(dual_model, MOI.ConstraintSet(), dual_con)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need MOI.copy just like in get_canonical_complement

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now done, but only for the dual_func. This is the only necessary one, right?
I'm asking because the copy is done for sets etc. in other functions (but that should be handled by set_with_zero, right?

PS: This was a big one, so I'm a bit surprised it didn't affect testing...

if MOI.constant(dual_set) > 0
dual_func.constant = dual_func.constant - MOI.constant(dual_set)
elseif MOI.constant(dual_set) < 0
dual_func.constant = dual_func.constant + MOI.constant(dual_set)
end
return Complement(
false,
primal_con,
dual_func,
set_with_zero(dual_set),
primal_variable,
)
end

function get_variable_complement(
primal_model,
dual_model,
primal_con::MOI.ConstraintIndex{Fp,Sp},
dual_con::MOI.ConstraintIndex{Fd,Sd},
) where {Fp<:MOI.VectorOfVariables,Sp<:VECTOR_SETS,Fd,Sd<:VECTOR_SETS} where {T}
primal_variables = MOI.copy(
MOI.get(primal_model, MOI.ConstraintFunction(), primal_con),
)::Fp
primal_set =
MOI.copy(MOI.get(primal_model, MOI.ConstraintSet(), primal_con))::Sp

dual_func =
MOI.copy(MOI.get(dual_model, MOI.ConstraintFunction(), dual_con))
dual_set = MOI.copy(MOI.get(dual_model, MOI.ConstraintSet(), dual_con))

# Do we have a todo here as in function get_canonical_complement(primal_model, map, ci::CI{F,S}) where {F, S<:VECTOR_SETS} ??
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do what?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was the following todo in the code of get_canonical_complement (note that I commented one additional line out):

    # dim = MOI.dimension(set)
    # vector sets have no constant
    # for i in 1:dim
    #     func.constant[i] = Dualization.set_dot(i, set, T) *
    #         Dualization.get_scalar_term(primal_model, ci, i)
    # end
    # todo - set dot on function


con = Complement(
true,
primal_con,
dual_func,
set_with_zero(dual_set),
primal_variables.variables,
)
return con
end

function get_variable_complements(primal_model, dual_model, primal_dual_map)
map = primal_dual_map.primal_con_dual_var
out = Complement[]
for (primal_con, dual_con) in primal_dual_map.constrained_var_dual
con = get_variable_complement(
primal_model,
dual_model,
primal_con,
dual_con,
)
push!(out, con)
end
return out
end

function get_canonical_complements(primal_model, primal_dual_map)
map = primal_dual_map.primal_con_dual_var
out = Complement[]
Expand Down Expand Up @@ -353,6 +435,7 @@ function build_bilevel(
upper_var_to_lower_ctr::Dict{VI,CI} = Dict{VI,CI}();
copy_names::Bool = false,
pass_start::Bool = false,
consider_constrained_variables::Bool = false,
)

# Start with an empty problem
Expand All @@ -371,7 +454,7 @@ function build_bilevel(
dual_names = DualNames("dual_", "dual_"),
variable_parameters = upper_variables,
ignore_objective = ignore_dual_objective(mode),
consider_constrained_variables = false,
consider_constrained_variables = consider_constrained_variables,
)
# the model
lower_dual = dual_problem.dual_model
Expand Down Expand Up @@ -491,6 +574,30 @@ function build_bilevel(
# feasible equality constraints always satisfy complementarity
end
end

if consider_constrained_variables
# complementary slackness for variable bounds
variable_comps = get_variable_complements(
lower,
lower_dual,
lower_primal_dual_map,
)
for comp in variable_comps
if !is_equality(comp.set_w_zero)
accept_vector_set(mode, comp)
add_complement(
mode,
m,
comp,
lower_dual_idxmap,
lower_idxmap,
copy_names,
pass_start,
)
end
end
end

else # strong duality
add_strong_duality(
mode,
Expand Down
Loading