Skip to content

Integrate with POI to improve UX #262

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 28 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ LazyArrays = "5078a376-72f3-5289-bfd5-ec5146d43c02"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
MathOptSetDistances = "3b969827-a86c-476c-9527-bb6f1a8fbad5"
ParametricOptInterface = "0ce4ce61-57bf-432b-a095-efac525d185e"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[compat]
BlockDiagonals = "0.1"
Expand All @@ -22,4 +24,5 @@ JuMP = "1"
LazyArrays = "0.21, 0.22, 1"
MathOptInterface = "1.18"
MathOptSetDistances = "0.2.7"
ParametricOptInterface = "0.9.0"
julia = "1.6"
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,76 @@ examples, tutorials, and an API reference.

## Use with JuMP

Use DiffOpt with JuMP by following this brief example:
### DiffOpt-JuMP API with `Parameters`

```julia
using JuMP, DiffOpt, HiGHS

model = Model(
() -> DiffOpt.diff_optimizer(
HiGHS.Optimizer;
with_parametric_opt_interface = true,
),
)
set_silent(model)

p_val = 4.0
pc_val = 2.0
@variable(model, x)
@variable(model, p in Parameter(p_val))
@variable(model, pc in Parameter(pc_val))
@constraint(model, cons, pc * x >= 3 * p) #??? InvalidConstraintRef TODO
@objective(model, Min, 2x)
optimize!(model)
@show value(x) == 3 * p_val / pc_val

# the function is
# x(p, pc) = 3p / pc
# hence,
# dx/dp = 3 / pc
# dx/dpc = -3p / pc^2

# First, try forward mode AD

# differentiate w.r.t. p
direction_p = 3.0
MOI.set(model, DiffOpt.ForwardConstraintSet(), ParameterRef(p), direction_p)
DiffOpt.forward_differentiate!(model)
@show MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) == direction_p * 3 / pc_val

# update p and pc
p_val = 2.0
pc_val = 6.0
set_parameter_value(p, p_val)
set_parameter_value(pc, pc_val)
# re-optimize
optimize!(model)
# check solution
@show value(x) ≈ 3 * p_val / pc_val

# stop differentiating with respect to p
DiffOpt.empty_input_sensitivities!(model)
# differentiate w.r.t. pc
direction_pc = 10.0
MOI.set(model, DiffOpt.ForwardConstraintSet(), ParameterRef(pc), direction_pc)
DiffOpt.forward_differentiate!(model)
@show abs(MOI.get(model, DiffOpt.ForwardVariablePrimal(), x) -
-direction_pc * 3 * p_val / pc_val^2) < 1e-5

# always a good practice to clear previously set sensitivities
DiffOpt.empty_input_sensitivities!(model)
# Now, reverse model AD
direction_x = 10.0
MOI.set(model, DiffOpt.ReverseVariablePrimal(), x, direction_x)
DiffOpt.reverse_differentiate!(model)
@show MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(p)) == direction_x * 3 / pc_val
@show abs(MOI.get(model, DiffOpt.ReverseConstraintSet(), ParameterRef(pc)) -
-direction_x * 3 * p_val / pc_val^2) < 1e-5
```

### Low level DiffOpt-JuMP API:

A brief example:

```julia
using JuMP, DiffOpt, HiGHS
Expand Down
24 changes: 13 additions & 11 deletions docs/src/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
As of now, this package only works for optimization models that can be written either in convex conic form or convex quadratic form.


## Supported objectives & constraints - scheme 1
## Supported objectives & constraints - `QuadraticProgram` backend

For `QPTH`/`OPTNET` style backend, the package supports following `Function-in-Set` constraints:
For `QuadraticProgram` backend, the package supports following `Function-in-Set` constraints:

| MOI Function | MOI Set |
|:-------|:---------------|
Expand All @@ -26,9 +26,9 @@ and the following objective types:
| `ScalarQuadraticFunction` |


## Supported objectives & constraints - scheme 2
## Supported objectives & constraints - `ConicProgram` backend

For `DiffCP`/`CVXPY` style backend, the package supports following `Function-in-Set` constraints:
For the `ConicProgram` backend backend, the package supports following `Function-in-Set` constraints:

| MOI Function | MOI Set |
|:-------|:---------------|
Expand All @@ -50,18 +50,22 @@ and the following objective types:
| `VariableIndex` |
| `ScalarAffineFunction` |

Other conic sets such as `RotatedSecondOrderCone` and `PositiveSemidefiniteConeSquare` are supported through bridges.

## Creating a differentiable optimizer

## Creating a differentiable MOI optimizer

You can create a differentiable optimizer over an existing MOI solver by using the `diff_optimizer` utility.
```@docs
diff_optimizer
```

## Adding new sets and constraints
## Creating a differentiable JuMP model

The DiffOpt `Optimizer` behaves similarly to other MOI Optimizers
and implements the `MOI.AbstractOptimizer` API.
You initialize a differentiable JuMP model by using the `diff_model` utility.
```@docs
diff_model
```

## Projections on cone sets

Expand Down Expand Up @@ -104,6 +108,4 @@ In the light of above, DiffOpt differentiates program variables ``x``, ``s``, ``
- OptNet: Differentiable Optimization as a Layer in Neural Networks

### Backward Pass vector
One possible point of confusion in finding Jacobians is the role of the backward pass vector - above eqn (7), *OptNet: Differentiable Optimization as a Layer in Neural Networks*. While differentiating convex programs, it is often the case that we don't want to find the actual derivatives, rather we might be interested in computing the product of Jacobians with a *backward pass vector*, often used in backprop in machine learning/automatic differentiation. This is what happens in scheme 1 of `DiffOpt` backend.

But, for the conic system (scheme 2), we provide perturbations in conic data (`dA`, `db`, `dc`) to compute pertubations (`dx`, `dy`, `dz`) in input variables. Unlike the quadratic case, these perturbations are actual derivatives, not the product with a backward pass vector. This is an important distinction between the two schemes of differential optimization.
One possible point of confusion in finding Jacobians is the role of the backward pass vector - above eqn (7), *OptNet: Differentiable Optimization as a Layer in Neural Networks*. While differentiating convex programs, it is often the case that we don't want to find the actual derivatives, rather we might be interested in computing the product of Jacobians with a *backward pass vector*, often used in backpropagation in machine learning/automatic differentiation. This is what happens in `DiffOpt` backends.
5 changes: 5 additions & 0 deletions src/DiffOpt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import LazyArrays
import LinearAlgebra
import MathOptInterface as MOI
import MathOptSetDistances as MOSD
import ParametricOptInterface as POI
import SparseArrays

include("utils.jl")
include("product_of_sets.jl")
include("diff_opt.jl")
include("moi_wrapper.jl")
include("jump_moi_overloads.jl")
include("parameters.jl")

include("copy_dual.jl")
include("bridges.jl")
Expand All @@ -40,4 +42,7 @@ end

export diff_optimizer

# TODO
# add precompilation statements

end # module
19 changes: 19 additions & 0 deletions src/bridges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@
MOI.get(model, attr, bridge.vector_constraint),
)[1]
end

function MOI.set(

Check warning on line 47 in src/bridges.jl

View check run for this annotation

Codecov / codecov/patch

src/bridges.jl#L47

Added line #L47 was not covered by tests
model::MOI.ModelLike,
attr::ForwardConstraintFunction,
bridge::MOI.Bridges.Constraint.ScalarizeBridge{T},
value,
) where {T}
MOI.set.(model, attr, bridge.scalar_constraints, value)
return

Check warning on line 54 in src/bridges.jl

View check run for this annotation

Codecov / codecov/patch

src/bridges.jl#L53-L54

Added lines #L53 - L54 were not covered by tests
end

function MOI.get(

Check warning on line 57 in src/bridges.jl

View check run for this annotation

Codecov / codecov/patch

src/bridges.jl#L57

Added line #L57 was not covered by tests
model::MOI.ModelLike,
attr::ReverseConstraintFunction,
bridge::MOI.Bridges.Constraint.ScalarizeBridge,
)
return _vectorize(MOI.get.(model, attr, bridge.scalar_constraints))

Check warning on line 62 in src/bridges.jl

View check run for this annotation

Codecov / codecov/patch

src/bridges.jl#L62

Added line #L62 was not covered by tests
end

function MOI.get(
model::MOI.ModelLike,
attr::DiffOpt.ReverseConstraintFunction,
Expand Down
11 changes: 11 additions & 0 deletions src/diff_opt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ The output solution differentials can be queried with the attribute
"""
function forward_differentiate! end

"""
empty_input_sensitivities!(model::MOI.ModelLike)

Empty the input sensitivities of the model.
Sets to zero all the sensitivities set by the user with method such as:
- `MOI.set(model, DiffOpt.ReverseVariablePrimal(), variable_index, value)`
- `MOI.set(model, DiffOpt.ForwardObjectiveFunction(), expression)`
- `MOI.set(model, DiffOpt.ForwardConstraintFunction(), index, expression)`
"""
function empty_input_sensitivities! end

"""
ForwardObjectiveFunction <: MOI.AbstractModelAttribute

Expand Down
15 changes: 15 additions & 0 deletions src/jump_moi_overloads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@
return forward_differentiate!(JuMP.backend(model))
end

function empty_input_sensitivities!(model::JuMP.Model)
empty_input_sensitivities!(JuMP.backend(model))
return

Check warning on line 312 in src/jump_moi_overloads.jl

View check run for this annotation

Codecov / codecov/patch

src/jump_moi_overloads.jl#L310-L312

Added lines #L310 - L312 were not covered by tests
end

# MOI.Utilities

function reverse_differentiate!(model::MOI.Utilities.CachingOptimizer)
Expand All @@ -317,6 +322,11 @@
return forward_differentiate!(model.optimizer)
end

function empty_input_sensitivities!(model::MOI.Utilities.CachingOptimizer)
empty_input_sensitivities!(model.optimizer)
return

Check warning on line 327 in src/jump_moi_overloads.jl

View check run for this annotation

Codecov / codecov/patch

src/jump_moi_overloads.jl#L325-L327

Added lines #L325 - L327 were not covered by tests
end

# MOIB

function reverse_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer)
Expand All @@ -326,3 +336,8 @@
function forward_differentiate!(model::MOI.Bridges.AbstractBridgeOptimizer)
return forward_differentiate!(model.model)
end

function empty_input_sensitivities!(model::MOI.Bridges.AbstractBridgeOptimizer)
empty_input_sensitivities!(model.model)
return

Check warning on line 342 in src/jump_moi_overloads.jl

View check run for this annotation

Codecov / codecov/patch

src/jump_moi_overloads.jl#L340-L342

Added lines #L340 - L342 were not covered by tests
end
47 changes: 37 additions & 10 deletions src/moi_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

"""
diff_optimizer(optimizer_constructor)::Optimizer
diff_optimizer(optimizer_constructor)

Creates a `DiffOpt.Optimizer`, which is an MOI layer with an internal optimizer
and other utility methods. Results (primal, dual and slack values) are obtained
Expand All @@ -21,19 +21,35 @@
julia> model.add_constraint(model, ...)
```
"""
function diff_optimizer(optimizer_constructor)::Optimizer
optimizer =
MOI.instantiate(optimizer_constructor; with_bridge_type = Float64)
function diff_optimizer(
optimizer_constructor;
method = nothing,
with_parametric_opt_interface::Bool = false,
with_bridge_type = Float64,
with_cache::Bool = true,
)
optimizer = MOI.instantiate(
optimizer_constructor;
with_bridge_type = with_bridge_type,
)
# When we do `MOI.copy_to(diff, optimizer)` we need to efficiently `MOI.get`
# the model information from `optimizer`. However, 1) `optimizer` may not
# implement some getters or it may be inefficient and 2) the getters may be
# unimplemented or inefficient through some bridges.
# For this reason we add a cache layer, the same cache JuMP adds.
caching_opt = MOI.Utilities.CachingOptimizer(
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
optimizer,
)
return Optimizer(caching_opt)
caching_opt = if with_cache
MOI.Utilities.CachingOptimizer(
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
optimizer,
)
else
optimizer
end
if with_parametric_opt_interface
return POI.Optimizer(Optimizer(caching_opt; method = method))
else
return Optimizer(caching_opt; method = method)
end
end

mutable struct Optimizer{OT<:MOI.ModelLike} <: MOI.AbstractOptimizer
Expand All @@ -49,10 +65,16 @@
# sensitivity input cache using MOI like sparse format
input_cache::InputCache

function Optimizer(optimizer::OT) where {OT<:MOI.ModelLike}
function Optimizer(
optimizer::OT;
method = nothing,
) where {OT<:MOI.ModelLike}
output =
new{OT}(optimizer, Any[], nothing, nothing, nothing, InputCache())
add_all_model_constructors(output)
if method !== nothing
output.model_constructor = method

Check warning on line 76 in src/moi_wrapper.jl

View check run for this annotation

Codecov / codecov/patch

src/moi_wrapper.jl#L76

Added line #L76 was not covered by tests
end
return output
end
end
Expand Down Expand Up @@ -552,6 +574,11 @@
return forward_differentiate!(diff)
end

function empty_input_sensitivities!(model::Optimizer)
empty!(model.input_cache)
return
end

function _instantiate_with_bridges(model_constructor)
model = MOI.Bridges.LazyBridgeOptimizer(MOI.instantiate(model_constructor))
# We don't add any variable bridge here because:
Expand Down
Loading
Loading