-
Notifications
You must be signed in to change notification settings - Fork 27
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
base: master
Are you sure you want to change the base?
Consider variable bounds clean #191
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start!
- We need this to work with vector sets.
- Aggregation groups should just work (we can leave it for a little later)
- I don't think we should test everything twice in the final version (we can leave it there while we finish the code.
- Bilevel Conic requires vector sets
src/moi.jl
Outdated
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<:MOI.GreaterThan{T},Fd,Sd<: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) | ||
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.VariableIndex,Sp<:MOI.LessThan{T},Fd,Sd<:MOI.LessThan{T}} where T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these two functions look the same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this was an old relict as I wasn't sure about constants etc in the beginning. Now, one is deleted and the other is defined for union of lesser/greater. Equalities still throw an error.
src/moi.jl
Outdated
if !consider_constrained_variables || contains_only_scalar_sets(lower) | ||
add_strong_duality(mode, m, lower_primal_obj, lower_dual_obj, lower_idxmap, lower_dual_idxmap) | ||
else | ||
error("Only scalar sets in lower level allowed with consider_constrained_variables.") | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think any of this is needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true, this threw occasional errors earlier. Found out that this was related to missing dual variables for which start values could not be passed.
This is now resolved, since hints etc. are no longer passed if the dual to bounds does not exist in the first place...
jump_01vec(optimizer, mode = BilevelJuMP.SOS1Mode(), config = Config()) = _jump_01(optimizer, true, mode, config) | ||
jump_01(optimizer, mode = BilevelJuMP.SOS1Mode(), config = Config()) = _jump_01(optimizer, false, mode, config) | ||
function _jump_01(optimizer, vectorized::Bool, mode, config) | ||
jump_01vec(optimizer, mode = BilevelJuMP.SOS1Mode(), consider_constrained_variables = consider_constrained_variables, config = Config()) = _jump_01(optimizer, true, mode, consider_constrained_variables, config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be a field in config.
Also, I am not sure this should be run in all tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left this for now, as I'm not sure which tests to select best etc.
Personally, I don't think that running tests twice adds a huge amount of time, as almost everything should be compiled already and the problems are fairly small.
On my machine, running tests like this adds about 6 seconds (less than 7%)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could also do the subset, but I'm not sure which ones to select...
Do you have a preference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the overhead is only 7% for running twice, this would indeed be fine
test/runtests.jl
Outdated
jump_11b(solver.opt, solver.mode) | ||
# jump_12(solver.opt, solver.mode) | ||
# jump_14(solver.opt, solver.mode) | ||
for consider_constrained_variables in [true, false] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think this should be run in all tests that would lead to run everything twice.
I think we should select a subset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above
src/moi.jl
Outdated
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<:MOI.GreaterThan{T},Fd,Sd<:MOI.GreaterThan{T}} where T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason for not supporting other sets like vector sets?
you can follow the code in get_canonical_complement, as you have been doing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left this out because I did not really feel certain.
Now added, can you check if it is correct?
Also not sure about the todo (see code for the new function)
Note that CI will only run if you fix conflicts with master. |
can you tell me if I resolved the conflicts correctly? |
src/jump.jl
Outdated
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( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same function twice?
There was a problem hiding this comment.
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
src/jump.jl
Outdated
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done, see above
dual_con::MOI.ConstraintIndex{Fd,Sd}, | ||
) where { | ||
Fp<:MOI.VariableIndex, | ||
Sp<:Union{MOI.LessThan{T},MOI.GreaterThan{T}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SCALAR_SETS ?
There was a problem hiding this comment.
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?
src/moi.jl
Outdated
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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...
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} ?? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do what?
There was a problem hiding this comment.
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
…ukasBarner/BilevelJuMP.jl into consider_variable_bounds_clean
Quick heads up: I just noticed that there are still some problems with setting correct start values. However, considering #185, this PR is still an overall improvement. |
We have to figure out what the issues are with SOS1Mode and FortunyAmatMcCarlMode. Start values should be passed, this should not be hard. Start values are frequently ignored by MIP solvers (used in FortunyAmatMcCarlMode). How do you know they are not accepted? MIP solvers usually require starting points to absolutely all variables involved. If things don't work for existing functionality, we should have issues to discuss them separately. This PR should not be blocked by things that were not previously working, but it should be blocked by things that previously worked. |
Quick disclaimer: As I said, I'm pretty new to this and not 100% sure about everything. In case it is nonsense, please excuse the effort of reading through all of it... First a few replies to the text above:
100% agree with the first part (especially since it will be a heavily used functionality), but not so sure that this is a trivial issue (more in the second part...)
For smaller examples, start values are sometimes accepted. Using Gurobi, I get notifications when start values violate constraints (i.e. constraint X is violated by 60.08). This is usually the case for larger models.
My code in general does set all start values correctly, this can be verified when switching to ProductMode with Ipopt, where initial point primal infeasibility of the single level model constraints is about e-7. Note that this only holds if Now a few remarks to the existing functionality and why I'm not sure starting values are a trivial issue. Currently, dual start values to lower level variable bounds are not set. Some exceptions exist, such as FortunyAmatMcCarlMode, where duals are sometimes set to zero (which tbh I'm not sure is actually correct: If the constraint was tight, why do we set the dual variable start to 0? ): Lines 1181 to 1185 in 565c0ef
As mentioned in #185, model.ctr_lower does not contain variable bounds currently (see the issue for an MWE demonstrating this). I believe, that in the following lines, this leads to their dual starts not being passed at all (exception above): Lines 515 to 524 in 2d25399
Furhter, I believe another reason why I have failing tests on larger models with start values in FortunyAmatMcCarlMode could be: Lines 1158 to 1167 in 565c0ef
Potentially, numerical imprecision and solver tolerances may lead to deviations larger than 1e-8, falsely classifying a constraint as not tight. I know this to be true in my case, but I'm not sure its the only issue. Another way to set the start values of binaries could be to take a look at the dual start values and check which deviation from zero of either the function or the dual variable is bigger. A problem though is that the dual starts are not yet available in this part of the code... These lines are actually also a problem for the newly introduced variable complements: Lines 1158 to 1167 in 565c0ef
Since duals do not yet have start values set, val may evaluate to NaN for the new variable complements despite all starts being set correctly...
For these reasons, I think it might make sense to already pass start values of duals in the 'build_bilevel' function before starting to set up complementary slackness (and to make sure that all starts are actually passed of course :D). As you can see, there are a few issues that can be discussed, but I'm afraid it would blow up this PR... PS: I have not taken a deeper dive into SOS1, but I figure similar issues are going on there... |
@joaquimg, did you have a chance to take a look the comment above yet? |
You were right here! There is something more fundamental... |
Okay, think I have figured this one out... Things that are fundamentally wrong (but easy to fix)Obvious mistake in this PR:First the general mistake, these lines I have written are obvious nonsense: 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 They should instead be: dual_func.constant = dual_func.constant - MOI.constant(dual_set) Passing of dual starts:Currently, dual starts are passed after the bilevel problem is built: Lines 571 to 598 in 565c0ef
This is fine if we do not have the new "variable complements". However, the new "variable complements" also have dual variables inside the ConstraintFunction, and some start value related functionalities (i.e. setting starting values of binary variables) rely upon evaluating starts: Lines 1158 to 1167 in 565c0ef
Since the dual start values are not yet passed for the variable complements, this tanks the whole warmstart procedure for MIP solvers that require fully feasibly start values (IMO usually the case). To make warmstarts work here, we have to move passing dual starts into the build_bilevel(...) function. Luckily, this is just copy-pasting and works fine. We need to pass the bilevel model as an argument to build_bilevel and then copy this piece Lines 586 to 609 in 565c0ef
and paste it here: Lines 460 to 464 in 565c0ef
Start values for SOS1 and FortunyAmatMcCarlNow to the issue with start values for SOS1 and FortunyAmatMcCarl: SOS1 Starts:Currently, no start values are set for the newly added slack variables here: Lines 765 to 803 in 565c0ef
This also holds if we do not consider constrained variables during dualization. However, it can easily be fixed by something like this inside the add_complement(...) function: slack_start = MOIU.eval_variables(
x -> nothing_to_nan(MOI.get(m, MOI.VariablePrimalStart(), x)),
f_dest,
)
MOI.set(m, MOI.VariablePrimalStart(), slack, slack_start) Now, SOS1 start values work as expected! FortunyAmatMcCarl Starts:Here, binary starts (and duals to zero) were mixed up: Lines 1182 to 1187 in 565c0ef
Which I believe should instead be: if pass_start && has_start && is_tight
MOI.set(m, MOI.VariablePrimalStart(), bin, 0.0)
else
MOI.set(m, MOI.VariablePrimalStart(), bin, 1.0)
MOI.set(m, MOI.VariablePrimalStart(), dual, 0.0)
end Since MIP solvers usually require a complete set of starts, I believe that user provided duals should not be over-written, so if pass_start && has_start && is_tight
MOI.set(m, MOI.VariablePrimalStart(), bin, 0.0)
else
MOI.set(m, MOI.VariablePrimalStart(), bin, 1.0)
end might even be a better solution... Finally, this line can make problems when providing warmstarts: Line 1164 in 565c0ef
a) For larger models, it is likely that some numerical imprecision occurs. b) If warmstarts are obtained by solving the lower level for fixed upper level variables, it is not guaranteed that solver tolerances are tighter than 1e-8 (Gurobi for example uses 1-6 by default). One workaround would be to make the tolerance an argument that can be specified by the user. is_tight = abs(val) < abs(nothing_to_nan(MOI.get(m, MOI.VariablePrimalStart(), dual))) |
Your comments seem reasonable indeed. |
Ok, decided to keep them in this PR. This should now also fix #185. |
Sorry for the mess on #189, I'm really not experienced in this at all :D
This is a clean version including all old tests now for both consider_constrained_variables true and false.
Still some failing tests for "Bilevel Conic JuMP NLP" and "Sum aggregation_group".
Since some tests in these testsets failed also on master (for me), "Bilevel Conic JuMP NLP" does not seem to be a problem. However I'm not sure about "Sum aggregation_group"...
I guess it could make sense to limit aggregation_group when working with consider_constrained_variables ?
Please let me know if there is anything in terms of style etc. so I can improve.