Skip to content

RFC: new modification syntax #388

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

Merged
merged 27 commits into from
Jun 20, 2018
Merged

RFC: new modification syntax #388

merged 27 commits into from
Jun 20, 2018

Conversation

odow
Copy link
Member

@odow odow commented Jun 14, 2018

Overview

This pull request changes the modification syntax in MOI.

The biggest change is to refactor modifyconstraint!(m, c::ConstraintIndex{F,S}, set::S) to set!(m, ::ConstraintSet, c::ConstraintIndex{F,S}, set::S). (A similar refactor is done with ConstraintFunction.) This plays much nicer with the get/set! paradigm.

In addition, modifyconstraint! methods that implement AbstractFunctionModification is refactored to modify! and transformconstraint! is refactored into transform!.

This PR also adds tests and documentation.

Changes

See #360 for discussion. Proposed changes:

  • set!(m::ModelLike, ::ConstraintFunction, c::ConstraintIndex{F,S}, new_function::F)
  • set!(m::ModelLike, ::ConstraintSet, c::ConstraintIndex{F,S}, new_set::S)
    @blegat, how do I make the change for ConstraintSet in model.jl? There is quite a few levels of in-direction and the macro and I don't know what to touch.
  • modify!(m::ModelLike, c::ConstraintIndex{F,S}, change::AbstractFunctionModification)
    (Note: this is slightly different to that proposed in Proposal: change API for modifications #360.)
  • modify!(m::ModelLike, ::ObjectiveFunction, change::AbstractFunctionModification)
  • transformconstraint! -> transform!

Tests

  • ConstraintSet
    • changing variable bound
    • changing rhs in scalaraffine
  • ConstraintFunction
  • modify! objective
    • scalarconstant
    • scalarcoefficient
  • modify! constraint
    • scalarconstant
      Given that we say the constant should be part of the set, what F-in-S pair makes sense to test?
    • vectorconstant
    • multirowchange
    • scalarcoefficient
  • transform!
    Not tested. See below

Blockers / future PR

  • cantransform still takes the instance of the constraint index of an argument rather than the type. This is needed because there is a default fallback and candelete also takes the instance rather than the type (see candelete is inconsistent with canget #218).
  • transform! is not tested as it is not implemented in MockOptimizer.
  • transform! only applies to constraint sets. Should it apply to constraint functions for consistency? I'm not sure if it makes sense to change the function to a different type of function without deleting the constraint though...

These points can probably be addressed by a future PR that changes candelete and cantransform at the same time and adds tests. This PR is already quite large.

`set!(m, ConstraintSet(), c, set)`. This patch currently errors as
it needs to be implemented for src/Utilities/model.jl, but I'm not
sure where to start.
@blegat
Copy link
Member

blegat commented Jun 14, 2018

Do we now disallow a set or function being passed to modify! ?

@odow
Copy link
Member Author

odow commented Jun 14, 2018

Yes. modify will be reserved for AbstractFunctionModifications.

function MOI.modifyconstraint!(mock::MockOptimizer, c::CI{F,S}, set::S) where {F<:MOI.AbstractFunction, S<:MOI.AbstractSet}
MOI.modifyconstraint!(mock.inner_model, xor_index(c), set)
MOI.canset(mock::MockOptimizer, ::MOI.ConstraintSet, c::CI{F,S}, set::Type{S}) where {F<:MOI.AbstractFunction, S<:MOI.AbstractSet} = true
function MOI.set!(mock::MockOptimizer, ::MOI.ConstraintSet, c::CI{F,S}, set::S) where {F<:MOI.AbstractFunction, S<:MOI.AbstractSet}
Copy link
Member

Choose a reason for hiding this comment

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

This is already handled by line 94

set!(model, ConstraintSet(), c, NonPositives) # Error
```
"""
function set!(model::ModelLike, cs::ConstraintSet, c::ConstraintIndex{F,S1}, s::S2) where F where S1 where S2
Copy link
Member

Choose a reason for hiding this comment

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

The issue of having a specific method like this is that it prevents models to handle it with a more general method as this would lead to ambiguity (this affects mockoptimizer, bridges, ...)
We could instead call something like seterror in line 234.
The default seterror method would give the ArgumentError currently at line 234 and we would add this method as a seterror method.

@blegat
Copy link
Member

blegat commented Jun 14, 2018

Why don't we use a different signature for canset than the one used for all other attributes ?

@blegat blegat mentioned this pull request Jun 14, 2018
@codecov-io
Copy link

codecov-io commented Jun 14, 2018

Codecov Report

Merging #388 into master will increase coverage by 0.15%.
The diff coverage is 93.22%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #388      +/-   ##
==========================================
+ Coverage   96.35%   96.51%   +0.15%     
==========================================
  Files          39       40       +1     
  Lines        4913     5676     +763     
==========================================
+ Hits         4734     5478     +744     
- Misses        179      198      +19
Impacted Files Coverage Δ
src/MathOptInterface.jl 100% <ø> (ø) ⬆️
src/Test/UnitTests/unit_tests.jl 100% <ø> (ø) ⬆️
src/modifications.jl 0% <0%> (ø)
src/attributes.jl 84.61% <0%> (-9.01%) ⬇️
src/Utilities/mockoptimizer.jl 99.5% <100%> (+2.01%) ⬆️
src/Test/UnitTests/modifications.jl 100% <100%> (ø)
src/Bridges/rsocbridge.jl 100% <100%> (ø) ⬆️
src/Bridges/detbridge.jl 98.82% <100%> (+0.05%) ⬆️
src/Test/contlinear.jl 100% <100%> (ø) ⬆️
src/Utilities/model.jl 98.73% <100%> (+0.02%) ⬆️
... and 16 more

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 ef32b86...622f009. Read the comment docs.

if isbridged(b, C)
MOI.canmodify(b.bridged, C, Chg)
# TODO(@blegat): how should the following be implemented?
# && MOI.canmodify(b, MOIB.bridge(b, ci), Chg)
Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, @blegat new question. Is this necessary and in what use-case? It makes tests like
https://github.com/JuliaOpt/MathOptInterface.jl/pull/388/files#diff-5d9f4af23e8ec976db542b6b9af239c4R135
break, but everything else passes. There are also things like
https://github.com/JuliaOpt/MathOptInterface.jl/pull/388/files#diff-91d11cfb910f6ae0036ae89cac114d3eR144
Are they necessary?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it checks that the bridge supports the modification. Currently, only the interval bridge supports modification

Copy link
Member Author

Choose a reason for hiding this comment

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

I missed the second part to the question. If they are necessary, how do we implement this using only type information...

Copy link
Member

Choose a reason for hiding this comment

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

You need bridgetype :)

Copy link
Member

Choose a reason for hiding this comment

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

I have pushed a fix

if isbridged(b, C)
MOI.canset(b.bridged, MOI.ConstraintSet(), C)
# TODO(@blegat) is this necessary? How do I do it without types
# && MOI.canset(b, MOIB.bridge(b, ci), change)
Copy link
Member Author

Choose a reason for hiding this comment

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

Ditto here.

if isbridged(b, C)
MOI.canset(b.bridged, MOI.ConstraintFunction(), C)
# TODO(@blegat) is this necessary? How do I do it without types
# && MOI.canset(b, MOIB.bridge(b, ci), change)
Copy link
Member Author

Choose a reason for hiding this comment

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

And here.

)
@test MOI.canget(model, MOI.ConstraintFunction(), typeof(c))
foo = MOI.get(model, MOI.ConstraintFunction(), c)
@test_broken foo == MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(2.0, x)], 0.0)
Copy link
Member Author

Choose a reason for hiding this comment

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

@blegat another mystery. The VariableIndex in foo is VariableIndex(12345679) instead of VariableIndex(1), but I couldn't work out where the missing xor is.

Copy link
Member

Choose a reason for hiding this comment

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

Fixed by 1ad6f61

end
end
function MOI.set!(b::AbstractBridgeOptimizer, ::MOI.ConstraintFunction, constraint_index::CI, func)
# Note: we also under-type this function to avoid ambiguity
Copy link
Member

Choose a reason for hiding this comment

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

What do you mean by this comment ? Why don't you just define set!(..., attr::Union{ConstraintFunction, ConstraintSet}, ...) ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I had a hard time getting rid of method ambiguities since there are quite a few abstract subtypes of ModelLike floating around. With the Union there is an ambiguity with set!(::ModelLike, ::ConstraintFunction, ...). as the first argument is more specific but the second is less specific. I've typed this now and it seems to all work.

set!(m, ConstraintSet(), c, GreaterThan(2.0)) # errors
```
If our constraint is an affine inequality, then this corresponds to modifying
the right hand-side of a constraint in linear programming.
Copy link
Member

Choose a reason for hiding this comment

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

Nit: right-hand side

Also note that `transform!` cannot be called with a set of the same type;
[`set!`](@ref) should be used instead.

### Modify the function of a constraint
Copy link
Member

Choose a reason for hiding this comment

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

The use of "modify" here could be a bit confusing given that this is discussing replacing the function rather than using an AbstractFunctionModification. It maybe be worth restructuring a bit to clarify, e.g., by grouping all the AbstractFunctionModifications together somehow.


### MultirowChange

Finally, the last modification supports by MathOptInterface is the ability to
Copy link
Member

Choose a reason for hiding this comment

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

Nit: supported

### MultirowChange

Finally, the last modification supports by MathOptInterface is the ability to
modify the affine coefficient of a single variable in a VectorAffine or
Copy link
Member

Choose a reason for hiding this comment

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

affine coefficients

### MultirowChange

Finally, the last modification supports by MathOptInterface is the ability to
modify the affine coefficient of a single variable in a VectorAffine or
Copy link
Member

Choose a reason for hiding this comment

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

It's a bit clearer to say more explicitly "in a VectorAffineFunction or a VectorQuadraticFunction".

function MOI.canset(uf::UniversalFallback, ::MOI.ConstraintSet, ::Type{C}) where C <: CI
MOI.canset(uf.model, MOI.ConstraintSet(), C)
end
function MOI.set!(uf::UniversalFallback, ::MOI.ConstraintSet, ci::CI{F,S}, set::S) where F where S
Copy link
Member

Choose a reason for hiding this comment

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

I think where {F,S} is more idiomatic than where F where S, but correct me if I'm wrong.

function canmodifyconstraint end
canmodifyconstraint(model::ModelLike, c::ConstraintIndex, change) = false
canset(model::ModelLike, ::ConstraintSet, type_of_constraint_set) = false
# Note: the above method is deliberately under-typed to avoid method ambiguities
Copy link
Member

Choose a reason for hiding this comment

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

Add an example of which method ambiguity is being avoided, in case we refactor later and it's no longer necessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added.

modifyconstraint!(model, c, Interval(0, 5))
modifyconstraint!(model, c, NonPositives) # Error
set!(model, ConstraintSet(), c, Interval(0, 5))
set!(model, ConstraintSet(), c, NonPositives) # Error
Copy link
Member

Choose a reason for hiding this comment

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

Nonpositives is the type. Maybe use a scalar set instead so it's a bit more clear, e.g., LessThan(3).

cantransformconstraint(model, c, GreaterThan(0.0)) # true
cantransformconstraint(model, c, ZeroOne()) # false
cantransform(model, c, GreaterThan(0.0)) # true
cantransform(model, c, ZeroOne()) # false
Copy link
Member

Choose a reason for hiding this comment

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

This is false for all current solvers, but MOI allows you to implement a solver where this could work. Maybe clarify that this is expected behavior but not a hard rule.


## Objective Function

canmodify(model::ModelLike, ::ObjectiveFunction, ::Type{M})::Bool where M<:AbstractFunctionModification
Copy link
Member

Choose a reason for hiding this comment

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

Missing code formatting?

end
end

function MOI.canset(b::AbstractBridgeOptimizer, attr::MOI.ConstraintSet, ::Type{CI{F, S}}) where {F, S}
Copy link
Member

Choose a reason for hiding this comment

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

This can be merged with the ConstraintFunction or not finally ? I thought you said in your comment that it was updated

Copy link
Member Author

@odow odow Jun 18, 2018

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.

MOI.canmodify(uf::UniversalFallback, ci::Type{<:CI}, change) = MOI.canmodify(uf.model, ci, change)
MOI.modify!(uf::UniversalFallback, ci::CI, change) = MOI.modify!(uf.model, ci, change)

function MOI.canset(uf::UniversalFallback, ::MOI.ConstraintSet, ::Type{C}) where C <: CI
Copy link
Member

Choose a reason for hiding this comment

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

Again, do we really need 4 functions instead of two ?

```
"""
function set!(model::ModelLike, ::ConstraintSet, constraint_index, set)
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't define set! and canset methods with so specific types.
We should do set!(model::ModelLike, attr::AbstractConstraintAttribute, ci::CI, value) = default_set!(model, attr, ci, value) and then in the methods of default_set! we can do any typing we like without risk of ambiguity

@odow
Copy link
Member Author

odow commented Jun 18, 2018

  • I have moved canset and set! out of the constraints.jl file and added them to the original docstring.
  • I've also implemented @blegat's idea of a hidden_set! function which we can overload to throw errors without causing ambiguity.
  • I have also removed a few of the separate canset(m, ::ConstraintSet and canset(m, ::ConstraintFunction methods in favor of ::Union{ConstraintSet,ConstraintFunction} methods.

There are still a few unresolved tick-boxes in the description of this PR, but they can be probably be addressed in future PRs.

@@ -527,13 +578,35 @@ It is guaranteed to be equivalent but not necessarily identical to the function
"""
struct ConstraintFunction <: AbstractConstraintAttribute end

function hidden_set!(model::ModelLike, ::ConstraintFunction, constraint_index::ConstraintIndex, func::AbstractFunction)
if func_type(constraint_index) != typeof(func)
Copy link
Member

@blegat blegat Jun 19, 2018

Choose a reason for hiding this comment

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

This can be done with multiple dispatch,
hidden_set!(..., ::ConstraintFunction, ::CI{F}, ::F) -> same constraint set
hidden_set!(..., ::ConstraintFunction, ::CI, ::AbstractFunction) -> different constraint set

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh yeah good point. This is a hold-over from before

"""
ConstraintSet()

Return the `AbstractSet` object used to define the constraint.
"""
struct ConstraintSet <: AbstractConstraintAttribute end

function hidden_set!(model::ModelLike, ::ConstraintSet, constraint_index::ConstraintIndex, set::AbstractSet)
if set_type(constraint_index) != typeof(set)
Copy link
Member

Choose a reason for hiding this comment

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

Same, here, it is cleaner to do that using multiple dispatch

end

# This function avoids duplicating code, but allows us to strongly type the
# arguments of the set! functions below.
Copy link
Member

Choose a reason for hiding this comment

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

I didn't get that immediately, mentioning the 4th argument explicitly in the comment would be clearer

Copy link
Member

@blegat blegat left a comment

Choose a reason for hiding this comment

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

Looks good, the new API is a lot better :)

Copy link
Member

@mlubin mlubin left a comment

Choose a reason for hiding this comment

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

LGTM, just a couple additional minor comments.

)
```

### In-place Modifications
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Inconsistent capitalization of words in section titles. (Also "Problem Modification" at the top).

test/model.jl Outdated
@@ -123,3 +123,28 @@ end
@test MOI.dimension(s) == 1

end

struct Func388 <: MOI.AbstractScalarFunction
Copy link
Member

Choose a reason for hiding this comment

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

Nit: use a more descriptive name or add a link to the PR if we need to reference the discussion. 388 isn't too meaningful on its own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

5 participants