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

[WIP] POI + DiffOpt = S2 #143

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

[WIP] POI + DiffOpt = S2 #143

wants to merge 12 commits into from

Conversation

joaquimg
Copy link
Member

@joaquimg joaquimg commented Dec 4, 2023

@andrewrosemberg motivated me.

I love how well layers can play with each other.

This will not be merged (as part of POI src) as it does not make sense to add DiffOpt as a dep for POI.

This should be either:
1 - An extension here (POI)
2 - A separate package
3 - An extension at DiffOpt

Semantically, option 3 makes lots of sense. But this uses too much of POI internals. Option 2 has a similar issue, we will have to pin a POI version.
Currently, I like 1.

Still requires:

  • Reverse mode
  • More tests (objectives, vector affine, more constraints...)
  • Deal with cached data (a reset_input_sensitivities in DiffOpt would be handy)
  • Deal with type stability (add barriers)
  • Move it to the right place

cc @matbesancon, @blegat

Copy link

codecov bot commented Dec 4, 2023

Codecov Report

Attention: Patch coverage is 94.81481% with 14 lines in your changes missing coverage. Please review.

Project coverage is 95.31%. Comparing base (4ec565a) to head (1493fac).
Report is 10 commits behind head on master.

Files with missing lines Patch % Lines
src/diff.jl 94.77% 14 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #143      +/-   ##
==========================================
+ Coverage   94.96%   95.31%   +0.35%     
==========================================
  Files           5        6       +1     
  Lines        1032     1302     +270     
==========================================
+ Hits          980     1241     +261     
- Misses         52       61       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

return
end

struct ForwardParameter <: MOI.AbstractVariableAttribute end
Copy link
Member

Choose a reason for hiding this comment

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

Why having a different attribute ? We could just use ForwardVariablePrimal for parameters as well

Copy link
Member Author

Choose a reason for hiding this comment

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

We can consider that. On the other hand, ForwardVariablePrimal is for output sensitivity, while ForwardParameter is for input sensitivity. Having both different would be good for validation, since we gave up on names like: ForwardOutputVariablePrimal.

Copy link
Member

Choose a reason for hiding this comment

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

You can different between input and output depending on whether it's a set or a get. Note that defining a new struct or a new function isn't so natural for an extension, it's more designed to add methods for existing ones. However, it is possible, see the hack in NLopt.

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding more details to my previous comment:
I find it strange that:
For parameters:
set ForwardVariablePrimal sets an input value that can be get to check which value was there.
While, for actual variables:
set ForwardVariablePrimal always errors, and get ForwardVariablePrimal only makes sense after forward_differentiate!
This was the main motivation for the new attribute.

About:

Note that defining a new struct or a new function isn't so natural for an extension, it's more designed to add methods for existing ones.

I think I did not understand completely. DiffOpt adds new structs. Also, a few solvers define new structs (like Gurobi.NumberOfObjectives, GLPK.CallbackFunction, COSMO.ADMMIterations).

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah! now I get it, you mean Julia extensions like NLoptMathOptInterfaceExt.jl

Copy link
Member

@blegat blegat Dec 5, 2023

Choose a reason for hiding this comment

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

Yes, I meant Julia extensions :) The workaround in NLopt works quite well but it becomes tricky when you try to include their docstring in the documentation. It was quite hard to make it work in JuliaManifolds/Manopt.jl#264 for instance.
Also, as from the MOI level, these are also variables and the set can be bridged to EqualTo, it makes sense to consider it as ForwardVariablePrimal.

Actually, what we could do it add ForwardConstraintSet that is defined for MOI.Parameter, MOI.EqualTo, MOI.LessThan, MOI.GreaterThan, MOI.Interval. I think we worked around it in DiffOpt by using the constant in the function but if you have a VariableIndex-in-S then you can't modify the function right ?
We could disallow ForwardConstraintSet for non-VariableIndex to avoid having two ways to set the same thing. Even if that's not consistent with the ConstraintFunction/ConstraintSet attributes, that's backward compatible. Or we can change this and tag v0.5 of DiffOpt.

The advantage of this design is that we can implement ForwardConstraintSet in the bridge that transforms Parameter to EqualTo so that the same user code works with both a POI-based solver and a solver using the bridge.

Comment on lines +55 to +65
model = direct_model(POI.Optimizer(DiffOpt.diff_optimizer(SCS.Optimizer)))
set_silent(model)
@variable(model, x)
@variable(model, p in MOI.Parameter(3.0))
@constraint(model, cons, [x - 3 * p] in MOI.Zeros(1))

# FIXME
@constraint(model, fake_soc, [0, 0, 0] in SecondOrderCone())

@objective(model, Min, 2x)
optimize!(model)
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 , I had to add this fake SOC constraint to force it into the Conic model. Otherwise, it tries to use a quadratic model (as the model is linear) and then fails with an error about not being able to push the DiffOpt attributes through the scalarize bridge.

Copy link
Member

Choose a reason for hiding this comment

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

Many bridges are missing in DiffOpt but it's easy to add, open an issue

Copy link
Member

Choose a reason for hiding this comment

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

You can use DiffOpt.ModelConstructor to force it to use conic

@andrewrosemberg
Copy link

andrewrosemberg commented Dec 14, 2023

I am implementing a test that I believe should work:

function test_diff_projection()
    num_A = 2
    ##### SecondOrderCone #####
    _x_hat = rand(num_A)
    μ = rand(num_A) * 10
    Σ_12 = rand(num_A, num_A)
    Σ = Σ_12 * Σ_12' + 0.1 * I
    γ = 1.0
    model = direct_model(POI.Optimizer(DiffOpt.diff_optimizer(SCS.Optimizer)))
    set_silent(model)
    @variable(model, x[1:num_A])
    @variable(model, x_hat[1:num_A] in MOI.Parameter.(_x_hat))
    @variable(model, norm_2)
    # (x - x_hat)^T Σ^-1 (x - x_hat) <= γ
    @constraint(
        model,
        (x - μ)' * inv(Σ) * (x - μ) <= γ,
    )
    # norm_2 >= ||x - x_hat||_2
    @constraint(model, [norm_2; x - x_hat] in SecondOrderCone())
    @objective(model, Min, norm_2)
    optimize!(model)
    MOI.set.(model, POI.ForwardParameter(), x_hat, ones(num_A))
    DiffOpt.forward_differentiate!(model) # ERROR
    #@test TBD
    return
end

But I am getting an error at the forward_differentiate step:

ERROR: MethodError: no method matching throw_set_error_fallback(::MathOptInterface.Bridges.LazyBridgeOptimizer{DiffOpt.ConicProgram.Model}, ::DiffOpt.ObjectiveFunctionAttribute{DiffOpt.ObjectiveDualStart, MathOptInterface.VariableIndex}, ::MathOptInterface.Bridges.Objective.FunctionConversionBridge{Float64, MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.VariableIndex}, ::Float64)

stacktrace:

Stacktrace:
 [1] set(::MathOptInterface.Bridges.LazyBridgeOptimizer{DiffOpt.ConicProgram.Model}, ::DiffOpt.ObjectiveFunctionAttribute{DiffOpt.ObjectiveDualStart, MathOptInterface.VariableIndex}, ::MathOptInterface.Bridges.Objective.FunctionConversionBridge{Float64, MathOptInterface.ScalarAffineFunction{Float64}, MathOptInterface.VariableIndex}, ::Float64)
   @ MathOptInterface ~/.julia/packages/MathOptInterface/IiXiU/src/attributes.jl:550
 [2] set(b::MathOptInterface.Bridges.LazyBridgeOptimizer{DiffOpt.ConicProgram.Model}, attr::DiffOpt.ObjectiveFunctionAttribute{DiffOpt.ObjectiveDualStart, MathOptInterface.VariableIndex}, value::Float64)
   @ DiffOpt ~/.julia/packages/DiffOpt/6Xx9R/src/copy_dual.jl:90
 [3] set(b::MathOptInterface.Bridges.LazyBridgeOptimizer{DiffOpt.ConicProgram.Model}, attr::DiffOpt.ObjectiveDualStart, value::Float64)
   @ DiffOpt ~/.julia/packages/DiffOpt/6Xx9R/src/copy_dual.jl:114
 [4] _copy_dual(dest::MathOptInterface.Bridges.LazyBridgeOptimizer{DiffOpt.ConicProgram.Model}, src::MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.Bridges.LazyBridgeOptimizer{MathOptInterface.Utilities.CachingOptimizer{SCS.Optimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}}, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}, index_map::MathOptInterface.Utilities.IndexMap)
   @ DiffOpt ~/.julia/packages/DiffOpt/6Xx9R/src/copy_dual.jl:176
 [5] _diff(model::DiffOpt.Optimizer{MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.Bridges.LazyBridgeOptimizer{MathOptInterface.Utilities.CachingOptimizer{SCS.Optimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}}, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}})
   @ DiffOpt ~/.julia/packages/DiffOpt/6Xx9R/src/moi_wrapper.jl:600
 [6] forward_differentiate!(model::DiffOpt.Optimizer{MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.Bridges.LazyBridgeOptimizer{MathOptInterface.Utilities.CachingOptimizer{SCS.Optimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}}, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}})
   @ DiffOpt ~/.julia/packages/DiffOpt/6Xx9R/src/moi_wrapper.jl:525
 [7] forward_differentiate!(model::ParametricOptInterface.Optimizer{Float64, DiffOpt.Optimizer{MathOptInterface.Utilities.CachingOptimizer{MathOptInterface.Bridges.LazyBridgeOptimizer{MathOptInterface.Utilities.CachingOptimizer{SCS.Optimizer, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}}, MathOptInterface.Utilities.UniversalFallback{MathOptInterface.Utilities.Model{Float64}}}}})
   @ ParametricOptInterface ~/Workspace/ParametricOptInterface.jl/src/diff.jl:222
 [8] forward_differentiate!(model::Model)
   @ DiffOpt ~/.julia/packages/DiffOpt/6Xx9R/src/jump_moi_overloads.jl:307
 [9] top-level scope
   @ REPL[48]:1

Edit: I imagine that this is a missing bridge right ?

@Giovanni3A
Copy link

I encountered an error while working with DiffOpt and POI. To demonstrate the problem, I created a minimal example:

This creates a simple problem using an explicitly indexed constraint (con[i=1:2]), then applies reverse_differentiate and it works:

using JuMP, DiffOpt, HiGHS
import ParametricOptInterface as POI

b = [1.0, 2.0]

m = Model(() -> POI.Optimizer(DiffOpt.diff_optimizer(HiGHS.Optimizer)))
@variable(m, x[1:2] >= 0)
@variable(m, c[1:2] in MOI.Parameter.(b))
@constraint(m, con[i=1:2], x[i] <= c[i])
@objective(m, Max, sum(x))
optimize!(m)

MOI.set(m, DiffOpt.ReverseVariablePrimal(), m[:x][1], 1.0)
DiffOpt.reverse_differentiate!(m)
MOI.get(m, POI.ReverseParameter(), m[:c][1])

>>> 1.0

but when I declare the constraint in a non-indexed fashion, like this:
@constraint(m, con, x <= c)

I get an error when calling DiffOpt.reverse_differentiate!(m):

ERROR: ArgumentError: Bridge of type `ScalarizeBridge` does not support accessing the attribute `DiffOpt.ReverseConstraintFunction()`.

The error still happens if the constraint is not declared with a name (con in this case).

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.

4 participants