Skip to content

Commit 5c8f4f4

Browse files
authored
Add conic to ridge sentitivity (#231)
* Quadratic functions with conic constraints * Fix format * Remove extra section * Move compat entries * Add docstring
1 parent 7abe1c4 commit 5c8f4f4

File tree

12 files changed

+553
-102
lines changed

12 files changed

+553
-102
lines changed

Project.toml

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,9 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
1717
[compat]
1818
BlockDiagonals = "0.1"
1919
ChainRulesCore = "1"
20-
HiGHS = "1"
21-
Ipopt = "1"
2220
IterativeSolvers = "0.9"
2321
JuMP = "1"
2422
LazyArrays = "0.21, 0.22, 1"
25-
MathOptInterface = "1"
26-
MathOptSetDistances = "0.2"
27-
SCS = "1"
23+
MathOptInterface = "1.14.1"
24+
MathOptSetDistances = "0.2.7"
2825
julia = "1.6"
29-
30-
[extras]
31-
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
32-
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
33-
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
34-
SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13"
35-
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
36-
37-
[targets]
38-
test = ["DelimitedFiles", "HiGHS", "Ipopt", "SCS", "Test"]

src/ConicProgram/ConicProgram.jl

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ const Form{T} = MOI.Utilities.GenericModel{
5050
DiffOpt.ProductOfSets{T},
5151
},
5252
}
53+
function MOI.supports(
54+
::Form{T},
55+
::MOI.ObjectiveFunction{F},
56+
) where {T,F<:MOI.AbstractFunction}
57+
return F === MOI.ScalarAffineFunction{T}
58+
end
5359

5460
"""
5561
Diffopt.ConicProgram.Model <: DiffOpt.AbstractModel
@@ -199,6 +205,8 @@ function _gradient_cache(model::Model)
199205
n = A.n
200206
N = m + n + 1
201207
# NOTE: w = 1.0 systematically since we asserted the primal-dual pair is optimal
208+
# `inv(M)((x, y, 1), (0, s, 0)) = (x, y, 1) - (0, s, 0)`,
209+
# see Minty parametrization in https://stanford.edu/~boyd/papers/pdf/cone_prog_refine.pdf
202210
(u, v, w) = (model.x, model.y - model.s, 1.0)
203211

204212
# find gradient of projections on dual of the cones
@@ -209,16 +217,16 @@ function _gradient_cache(model::Model)
209217
# -A 0 b;
210218
# -c' -b' 0;
211219
# ]
212-
# M = (Q- I) * B + I
220+
# M = (Q - I) * B + I
213221
# with B =
214222
# [
215-
# I . . # Πx = x because x is a solution and hence satistfies the constraints
216-
# . Dπv .
217-
# . . 1 # w >= 0, but in the solution x = 1
223+
# I . . # Πx = x because it projects on R^n
224+
# . Dπv . # Derivative of the projection onto the dual cones
225+
# . . 1 # Projection onto R_+ but w is 1 so the derivative is 1
218226
# ]
219227
# see: https://stanford.edu/~boyd/papers/pdf/cone_prog_refine.pdf
220228
# for the definition of Π and why we get I and 1 for x and w respectectively
221-
# K is defined in (5), Π in sect 2, and projection sin sect 3
229+
# K is defined in (5), Π in sect 2, and projections in sect 3
222230

223231
M = [
224232
SparseArrays.spzeros(n, n) (A'*Dπv) c

src/DiffOpt.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ include("diff_opt.jl")
2020
include("moi_wrapper.jl")
2121
include("jump_moi_overloads.jl")
2222

23+
include("copy_dual.jl")
2324
include("bridges.jl")
2425

2526
include("QuadraticProgram/QuadraticProgram.jl")

src/bridges.jl

Lines changed: 174 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
# Use of this source code is governed by an MIT-style license that can be found
44
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
55

6+
function MOI.get(
7+
model::MOI.ModelLike,
8+
::ObjectiveFunctionAttribute{ReverseObjectiveFunction},
9+
bridge::MOI.Bridges.Objective.SlackBridge,
10+
)
11+
return MOI.get(model, ReverseConstraintFunction(), bridge.constraint)
12+
end
13+
14+
function MOI.set(
15+
model::MOI.ModelLike,
16+
::ObjectiveFunctionAttribute{ForwardObjectiveFunction},
17+
bridge::MOI.Bridges.Objective.SlackBridge,
18+
value,
19+
)
20+
return MOI.set(model, ForwardConstraintFunction(), bridge.constraint, value)
21+
end
22+
623
function MOI.set(
724
model::MOI.ModelLike,
825
attr::ForwardConstraintFunction,
@@ -44,6 +61,23 @@ function MOI.set(
4461
return MOI.set(model, attr, bridge.constraint, mapped_func)
4562
end
4663

64+
function _variable_to_index_map(bridge)
65+
return Dict{MOI.VariableIndex,MOI.VariableIndex}(
66+
v => MOI.VariableIndex(i) for
67+
(i, v) in enumerate(bridge.index_to_variable_map)
68+
)
69+
end
70+
71+
function _U(func::MOI.VectorAffineFunction, n, variable_to_index_map)
72+
# x'Qx/2 + a'x + β is bridged into
73+
# (1, -a'x - β, U*x) in RSOC
74+
# where Q = U' * U
75+
# The linear part of `func` is `[0; -a'; U]`
76+
Ux = MOI.Utilities.eachscalar(func)[3:end]
77+
func_array = sparse_array_representation(Ux, n, variable_to_index_map)
78+
return func_array.terms
79+
end
80+
4781
function MOI.get(
4882
model::MOI.ModelLike,
4983
attr::DiffOpt.ReverseConstraintFunction,
@@ -53,55 +87,167 @@ function MOI.get(
5387
return MOI.Bridges.adjoint_map_function(typeof(bridge), func)
5488
end
5589

56-
function MOI.set(
90+
function MOI.get(
5791
model::MOI.ModelLike,
58-
attr::DiffOpt.ForwardConstraintFunction,
92+
attr::DiffOpt.ReverseConstraintFunction,
5993
bridge::MOI.Bridges.Constraint.QuadtoSOCBridge{T},
60-
diff::MOI.ScalarQuadraticFunction,
6194
) where {T}
62-
func = MOI.get(model, MOI.ConstraintFunction(), bridge.soc)
63-
index_map = index_map_to_oneto(func)
64-
index_map_to_oneto!(index_map, diff)
65-
n = length(index_map.var_map)
66-
func_array = sparse_array_representation(func, n, index_map)
67-
# x'Qx/2 + a'x + β is bridged into
68-
# (1, -a'x - β, U*x) in RSOC
69-
# where Q = U' * U
70-
# `func_array.terms` is `[0; -a'; U]`
71-
U = func_array.terms[3:end, :]
72-
diff_array = sparse_array_representation(diff, n, index_map)
73-
dU = Matrix(diff_array.quadratic_terms)
74-
dU = dU_from_dQ!(Matrix(diff_array.quadratic_terms), U)
75-
x = Vector{MOI.VariableIndex}(undef, n)
76-
for v in keys(index_map.var_map)
77-
x[index_map[v].value] = v
95+
variable_to_index_map = _variable_to_index_map(bridge)
96+
n = length(bridge.index_to_variable_map)
97+
U = _U(
98+
MOI.get(model, MOI.ConstraintFunction(), bridge.soc),
99+
n,
100+
variable_to_index_map,
101+
)
102+
Δfunc =
103+
convert(MOI.VectorAffineFunction{T}, MOI.get(model, attr, bridge.soc))
104+
filter!(Δfunc.terms) do t
105+
return haskey(variable_to_index_map, t.scalar_term.variable)
78106
end
79-
diff_aff = MOI.VectorAffineFunction{T}(
80-
MOI.VectorAffineTerm{T}[],
81-
zeros(T, size(dU, 1) + 2),
107+
aff = sparse_array_representation(
108+
-MOI.Utilities.eachscalar(Δfunc)[2],
109+
n,
110+
variable_to_index_map,
111+
)
112+
ΔU = _U(Δfunc, n, variable_to_index_map)
113+
ΔQ = ΔQ_from_ΔU!(ΔU, U)
114+
func = MatrixScalarQuadraticFunction(
115+
convert(VectorScalarAffineFunction{T,Vector{T}}, aff),
116+
ΔQ,
82117
)
83-
for t in diff.affine_terms
118+
index_map = MOI.Utilities.IndexMap()
119+
for (i, vi) in enumerate(bridge.index_to_variable_map)
120+
index_map[MOI.VariableIndex(i)] = vi
121+
end
122+
Δ = standard_form(IndexMappedFunction(func, index_map))
123+
if !bridge.less_than
124+
Δ = -Δ
125+
end
126+
return Δ
127+
end
128+
129+
function _quad_to_soc_diff(
130+
::MOI.ModelLike,
131+
bridge::MOI.Bridges.Constraint.QuadtoSOCBridge{T},
132+
diff::MOI.ScalarAffineFunction{T},
133+
) where {T}
134+
n = length(bridge.index_to_variable_map)
135+
diff_soc =
136+
MOI.VectorAffineFunction{T}(MOI.VectorAffineTerm{T}[], zeros(T, n + 2))
137+
for t in diff.terms
84138
push!(
85-
diff_aff.terms,
139+
diff_soc.terms,
86140
MOI.VectorAffineTerm(
87141
2,
88142
MOI.ScalarAffineTerm(-t.coefficient, t.variable),
89143
),
90144
)
91145
end
92-
diff_aff.constants[2] = -diff.constant
146+
diff_soc.constants[2] = -diff.constant
147+
return diff_soc
148+
end
149+
150+
function _quad_to_soc_diff(
151+
model::MOI.ModelLike,
152+
bridge::MOI.Bridges.Constraint.QuadtoSOCBridge{T},
153+
diff::MOI.ScalarQuadraticFunction{T},
154+
) where {T}
155+
variable_to_index_map = _variable_to_index_map(bridge)
156+
n = length(bridge.index_to_variable_map)
157+
U = _U(
158+
MOI.get(model, MOI.ConstraintFunction(), bridge.soc),
159+
n,
160+
variable_to_index_map,
161+
)
162+
# We assume `diff.quadratic_terms` only contains quadratic terms with
163+
# variables already in a quadratic term of the initial constraint
164+
# Otherwise, the `U` matrix will not be square so it's a TODO
165+
# We remove the affine terms here as they might have other variables not
166+
# in `variable_to_index_map`
167+
diff_quad = MOI.ScalarQuadraticFunction(
168+
diff.quadratic_terms,
169+
MOI.ScalarAffineTerm{T}[],
170+
zero(T),
171+
)
172+
diff_aff = MOI.ScalarAffineFunction(diff.affine_terms, diff.constant)
173+
diff_array =
174+
sparse_array_representation(diff_quad, n, variable_to_index_map)
175+
dU = dU_from_dQ!(Matrix(diff_array.quadratic_terms), U)
176+
diff_soc = _quad_to_soc_diff(model, bridge, diff_aff)
93177
for i in axes(dU, 1)
94178
for j in axes(dU, 2)
95179
if !iszero(dU[i, j])
96-
scalar = MOI.ScalarAffineTerm(dU[i, j], x[j])
97-
push!(diff_aff.terms, MOI.VectorAffineTerm(2 + i, scalar))
180+
scalar = MOI.ScalarAffineTerm(
181+
dU[i, j],
182+
bridge.index_to_variable_map[j],
183+
)
184+
push!(diff_soc.terms, MOI.VectorAffineTerm(2 + i, scalar))
98185
end
99186
end
100187
end
101-
MOI.set(model, attr, bridge.soc, diff_aff)
188+
return diff_soc
189+
end
190+
191+
function MOI.set(
192+
model::MOI.ModelLike,
193+
attr::DiffOpt.ForwardConstraintFunction,
194+
bridge::MOI.Bridges.Constraint.QuadtoSOCBridge{T},
195+
diff::MOI.AbstractScalarFunction,
196+
) where {T}
197+
if !bridge.less_than
198+
diff = -diff
199+
end
200+
diff_soc = _quad_to_soc_diff(model, bridge, diff)
201+
MOI.set(model, attr, bridge.soc, diff_soc)
102202
return
103203
end
104204

205+
"""
206+
ΔQ_from_ΔU!(ΔU, U)
207+
208+
Return the symmetric solution `ΔQ` of the matrix equation
209+
`triu(ΔU) = 2triu(U * ΔQ)`
210+
where `ΔU` and `U` are the two argument of the function.
211+
212+
This function overwrites the first argument `ΔU` to store the solution.
213+
The matrix `U` is not however modified.
214+
215+
The matrix `U` is assumed to be upper triangular.
216+
217+
We can exploit the structure of `U` here:
218+
219+
* If the factorization was obtained from SVD, `U` would be orthogonal
220+
* If the factorization was obtained from Cholesky, `U` would be upper triangular.
221+
222+
The MOI bridge uses Cholesky in order to exploit sparsity so we are in the
223+
second case.
224+
225+
We can find each column of `ΔQ` by solving a triangular linear system.
226+
"""
227+
function ΔQ_from_ΔU!(ΔU, U)
228+
n = LinearAlgebra.checksquare(ΔU)
229+
LinearAlgebra.rdiv!(ΔU, 2)
230+
for j in n:-1:1
231+
# FIXME MA does not support mutating subarray
232+
#MA.operate!(MA.sub_mul, view(ΔU, 1:j, j), view(U, 1:j, (j + 1):n), view(ΔU, j, (j+1):n))
233+
for row in 1:j
234+
for col in (j+1):n
235+
ΔU[row, j] -= U[row, col] * ΔU[j, col]
236+
end
237+
end
238+
_U = LinearAlgebra.UpperTriangular(view(U, 1:j, 1:j))
239+
LinearAlgebra.ldiv!(_U, view(ΔU, 1:j, j))
240+
end
241+
for j in 1:n
242+
for i in 1:(j-1)
243+
if i != j
244+
ΔU[j, i] = ΔU[i, j]
245+
end
246+
end
247+
end
248+
return ΔU
249+
end
250+
105251
"""
106252
dU_from_dQ!(dQ, U)
107253

0 commit comments

Comments
 (0)