Skip to content
This repository has been archived by the owner on Jun 14, 2020. It is now read-only.

Allow setting scalar affine constraint functions via the modification API #37

Merged
merged 26 commits into from
Sep 4, 2018

Conversation

rdeits
Copy link
Contributor

@rdeits rdeits commented Jul 17, 2018

Fixes #32
Fixes jump-dev/GLPK.jl#58

I'm not sure how to test this. There's a solve_func_scalaraffine_lessthan modification unit test in MOI.Test, but it doesn't seem to be run during the LQOI tests. I'd appreciate some help turning that on (and any other tests that are available).

@codecov-io
Copy link

codecov-io commented Jul 17, 2018

Codecov Report

Merging #37 into master will increase coverage by 1.06%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #37      +/-   ##
==========================================
+ Coverage   76.27%   77.34%   +1.06%     
==========================================
  Files          13       13              
  Lines        1252     1311      +59     
==========================================
+ Hits          955     1014      +59     
  Misses        297      297
Impacted Files Coverage Δ
src/constraints/vectoraffine.jl 95.78% <100%> (+1.94%) ⬆️
src/solver_interface.jl 66.66% <100%> (+6.66%) ⬆️
src/constraints/scalaraffine.jl 95.89% <100%> (+0.89%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4f75448...fc38846. Read the comment docs.

@odow
Copy link
Member

odow commented Jul 17, 2018

I'd appreciate some help turning that on

The modification tests need to be added to LQOI. See GLPK for an example
https://github.com/JuliaOpt/GLPK.jl/blob/9a9e9f4ec6ac5bcc1b9b1b1e1a79fe9d2b43efe5/test/MOIWrapper.jl#L23-L25
The problem is that the mock solver in LQOI doesn't test the solution (since solve=false)

config = MOIT.TestConfig(solve = false)

so it isn't well tested. This needs to improve. I usually test by running GLPK and Gurobi on the changes.

@joaquimg is probably the person who understands the current state of the tests the best.

@rdeits
Copy link
Contributor Author

rdeits commented Jul 17, 2018

Got it, thanks. I'll run glpk and gurobi with the modification tests on and open PRs to turn them on for those packages.

@rdeits
Copy link
Contributor Author

rdeits commented Jul 18, 2018

By the way, jump-dev/Gurobi.jl#125 restricts LQOI to version [0.1, 0.2), but LQOI master is version 0.2+. Is that restriction necessary?

@rdeits
Copy link
Contributor Author

rdeits commented Jul 18, 2018

PRs opened: jump-dev/GLPK.jl#56 jump-dev/Gurobi.jl#134

@joaquimg
Copy link
Member

The problem with testing solutions is that one need to gather all the intermediary solutions (objective, variable primal and dual and constraint primal and dual) from some solver like Gurobi.
The mock solver is "ready"(but not tested) to receive these vectors as input.
Doing that for the "linear tests" is strainghtforward, not sure about the unit tests, but should not be hard.

@rdeits
Copy link
Contributor Author

rdeits commented Jul 18, 2018

Hm, i just realized this isn't quite right, as I'm not handling a possible modification of the constant in the affine function. It doesn't seem to be possible to use a ScalarConstantChange in LQOI. Should I just replace the constraint set at the same time?

@joaquimg
Copy link
Member

You are planning to modify the sparsity pattern of the constraint?
About performance... I would guess its faster to delete the constraint and add a new than query the old constraint and modify all coefficients.
ScalarConstantChange should be use when modifying few coefficients IMO.

Should I just replace the constraint set at the same time?

I would just replace the set.

@rdeits
Copy link
Contributor Author

rdeits commented Jul 18, 2018

I suspect that we will often not change the sparsity pattern, but in general I want the code to produce the right result regardless. But you're right that actually getting the existing constraint function looks like it will be very expensive. We may just have to re-write our interface to use modify! instead of set!

for term in replacement.terms
var = term.variable_index
chg = MOI.ScalarCoefficientChange{Float64}(var, term.coefficient)
MOI.modify!(m, CI, chg)
Copy link
Contributor

Choose a reason for hiding this comment

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

This also doesn't seem to support duplicate coefficients.

chg = MOI.ScalarCoefficientChange{Float64}(var, term.coefficient)
MOI.modify!(m, CI, chg)
end
change_rhs_coefficient!(m, m[CI], -replacement.constant)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this handle the constant encoded in the set properly (e.g. MOI.EqualTo(1.0))?

@tkoolen
Copy link
Contributor

tkoolen commented Jul 18, 2018

Some thoughts:

  • there should probably be an LQOI.change_matrix_coefficients! (plural) to reduce the number of low level API calls
  • at least for Gurobi, it seems that sparsity pattern changes are already handled internally. It's possible that simply zeroing everything (as opposed to just the currently-nonzero coefficients) is already efficient, so that getting the constraint function from the solver is not necessary.
  • something like OSQP's MatrixModificationCache is probably needed to handle duplicate coefficients in an (at least algorithmically) efficient way (note that this is actually a moderately slow part of the OSQP interface due to lots of Dict lookups).

@odow
Copy link
Member

odow commented Aug 6, 2018

What's the status of this? Is it necessary for the next release of LQOI? Or can it wait?

@rdeits rdeits changed the title allow setting scalar affine constraint functions via the modification API WIP: allow setting scalar affine constraint functions via the modification API Aug 6, 2018
@rdeits
Copy link
Contributor Author

rdeits commented Aug 6, 2018

Sorry, I haven't had time to vet this properly, and I think its handling of scalar affine functions is incorrect. It's on my todo list but not quite at the top.

If you're OK with leaving this open, I'll let you know when it's ready for review.

@rdeits
Copy link
Contributor Author

rdeits commented Aug 6, 2018

But to answer your question, no, this should not be necessary for the next release. It's just adding a new feature.

@rdeits
Copy link
Contributor Author

rdeits commented Aug 7, 2018

@odow will this change also be rendered moot by your switch to using bridges here? I'm just trying to decide whether to keep this as a local modification for my own work or to actually put in the time to finish it correctly.

@odow
Copy link
Member

odow commented Aug 7, 2018

I would keep it as a local change for now. Other than you, no one is clamoring for this change, so it doesn't make sense to spend a lot of time on it.

@rdeits rdeits force-pushed the rd/set-constraint-func branch from 9389980 to 06c8289 Compare August 17, 2018 22:30
@rdeits
Copy link
Contributor Author

rdeits commented Aug 18, 2018

Hey @odow this is actually becoming a bit more pressing for my work. I believe I've fixed the bug with RHS coefficients that was previously in this branch, and the lack of this feature is currently blocking tkoolen/Parametron.jl#75

The scalar part of this PR is tested by jump-dev/GLPK.jl#56 and the vector part is tested by tkoolen/Parametron.jl#75

Would you be willing to consider merging this, even if it will eventually be replaced by the bridges?

@rdeits rdeits force-pushed the rd/set-constraint-func branch from bc4d5c0 to 2af6a7c Compare August 19, 2018 16:44
@rdeits
Copy link
Contributor Author

rdeits commented Aug 19, 2018

This time for sure...

I think this is ready for review (although we should merge #53 first, since this PR depends on that one). There are now enough tests that I'm reasonably confident it's doing the right thing.

Unfortunately, this code is going to be very inefficient, particularly for GLPK. The problem is that every call to GLPK's implementation of change_matrix_coefficient! requires loading an entire row from the solver, performing the update, and then setting that entire row. We have to do that load and store for every single element in the constraint function. This inefficiency would be completely resolved by having GLPK implement MOI.set(..., ::ConstraintFunction) itself, which could be done at a later date.

@rdeits rdeits changed the title WIP: allow setting scalar affine constraint functions via the modification API Allow setting scalar affine constraint functions via the modification API Aug 19, 2018
@odow
Copy link
Member

odow commented Aug 19, 2018

I've merged #53. Will take a look at this

@@ -196,18 +196,65 @@ function MOI.modify!(model::LinQuadOptimizer, index::LCI{S}, change::MOI.ScalarC
change_matrix_coefficient!(model, row, column, change.new_coefficient)
end

function _replace_with_matching_sparsity!(model::LinQuadOptimizer, previous::Linear, replacement::Linear, row)
Copy link
Member

Choose a reason for hiding this comment

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

I know the rest of the code doesn't, but can we add docstrings for these new functions to remind ourselves of why they are needed and what they do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

rows = fill(row, length(replacement.terms))
cols = [model.variable_mapping[t.variable_index] for t in replacement.terms]
coefs = MOIU.coefficient.(replacement.terms)
change_matrix_coefficients!(model, rows, cols, coefs)
Copy link
Member

Choose a reason for hiding this comment

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

Do these account for duplicate coefficients? (Edit: looks from below that the inputs are canonicalized. All the more reason for docstrings.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've made a note of that assumption in the various docstrings

end
change_rhs_coefficient!(model, row, get_rhs(model, row) - (replacement.constant - previous.constant))
model.constraint_constant[model[CI]] = replacement.constant
nothing
Copy link
Member

Choose a reason for hiding this comment

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

Nit: return or return nothing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@@ -76,6 +76,62 @@ function MOI.modify!(model::LinQuadOptimizer, index::VLCI{<: VecLinSets},
end
end

_constant(f::Linear) = f.constant
_constant(f::VecLin) = f.constants
Copy link
Member

Choose a reason for hiding this comment

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

Are there MOIU functions for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are, but they're likewise _-prefixed so I didn't want to use them externally. MOIU does have constant() but it unnecessarily allocates a vector for scalar functions.

@codecov-io
Copy link

codecov-io commented Aug 20, 2018

Codecov Report

Merging #37 into master will increase coverage by 0.83%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #37      +/-   ##
==========================================
+ Coverage   76.27%   77.11%   +0.83%     
==========================================
  Files          13       13              
  Lines        1252     1315      +63     
==========================================
+ Hits          955     1014      +59     
- Misses        297      301       +4
Impacted Files Coverage Δ
src/constraints/vectoraffine.jl 95.78% <100%> (+1.94%) ⬆️
src/constraints/scalaraffine.jl 95.89% <100%> (+0.89%) ⬆️
src/solver_interface.jl 63.15% <100%> (+3.15%) ⬆️
src/constraints/singlevariable.jl 75.8% <0%> (ø) ⬆️
src/solve.jl 0% <0%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4f75448...1cb27fa. Read the comment docs.

@odow odow requested a review from joaquimg August 20, 2018 04:28
Copy link
Member

@odow odow left a comment

Choose a reason for hiding this comment

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

Thanks @rdeits

@@ -183,7 +183,7 @@ function MOI.get(model::LinQuadOptimizer, ::MOI.ConstraintFunction, index::LCI{<
model.variable_references[columns],
coefficients
)
Linear(terms, -model.constraint_constant[row])
Linear(terms, model.constraint_constant[row])
Copy link
Member

Choose a reason for hiding this comment

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

This was not touched by tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

nice job

@joaquimg
Copy link
Member

Looks good to go. I just added one question about the sign change.

@tkoolen
Copy link
Contributor

tkoolen commented Sep 4, 2018

Ready to go?

@rdeits
Copy link
Contributor Author

rdeits commented Sep 4, 2018

Yeah, I think this should be ready. Can we merge it?

Copy link
Contributor

@tkoolen tkoolen left a comment

Choose a reason for hiding this comment

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

Actually, indices should be changed to Compat.axes in a couple of places for 1.0 compatibility.

in canonical form.
"""
function _matching_sparsity_pattern(f1::T, f2::T) where {T <: Union{Linear, VecLin}}
indices(f1.terms) == indices(f2.terms) || return false
Copy link
Contributor

Choose a reason for hiding this comment

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

indices -> Compat.axes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

some solver interfaces may offer more efficient implementations.
"""
function change_matrix_coefficients!(m, rows, cols, coefs)
@boundscheck((indices(rows) == indices(cols) == indices(coefs)) || throw(DimensionMismatch()))
Copy link
Contributor

Choose a reason for hiding this comment

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

indices -> Compat.axes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@odow
Copy link
Member

odow commented Sep 4, 2018

Merging this. We can rebase #60 on top of this and address any other compat issues.

@odow odow merged commit 68fffb1 into JuliaOpt:master Sep 4, 2018
@odow odow mentioned this pull request Sep 4, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants