Skip to content

Commit 28ea73b

Browse files
authored
Add solution inspection to example (#24)
* Add solution inspection to example * Fix format * Finish README * Fix format * Add tests * Fix format
1 parent b19fcf2 commit 28ea73b

File tree

11 files changed

+298
-38
lines changed

11 files changed

+298
-38
lines changed

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ adapted to exploit the low-rank constraints.
4343
julia> include(joinpath(dirname(dirname(pathof(LowRankOpt))), "examples", "maxcut.jl"))
4444
maxcut (generic function with 2 methods)
4545
46+
julia> weights = [0 5 7 6; 5 0 0 1; 7 0 0 1; 6 1 1 0];
47+
4648
julia> model = maxcut(weights, SDPLR.Optimizer);
4749
4850
julia> optimize!(model)
@@ -68,4 +70,71 @@ DIMACS error measures: 5.86e-06 0.00e+00 0.00e+00 0.00e+00 2.25e-05 9.84e-06
6870
6971
julia> objective_value(model)
7072
17.99998881724702
73+
74+
julia> con_ref = VariableInSetRef(model[:dot_prod_set]);
75+
```
76+
77+
Use `LRO.InnerAttribute` to request the `SDPLR.Factor` attribute.
78+
```julia-repl
79+
julia> MOI.get(model, LRO.InnerAttribute(SDPLR.Factor()), VariableInSetRef(model[:dot_prod_set]))
80+
4×3 Matrix{Float64}:
81+
0.949505 0.31101 0.0414433
82+
-0.950269 -0.308646 -0.0415714
83+
-0.949503 -0.311095 -0.0406689
84+
-0.948855 -0.312898 -0.0420402
85+
```
86+
We can see that SDPLR decided to search for a solution of rank at most 3.
87+
To check if SDPLR found an optimal solution, we need to check whether the dual solution is feasible.
88+
This can be achieved as follows:
89+
```julia-repl
90+
julia> dual_set = MOI.dual_set(constraint_object(con_ref).set);
91+
92+
julia> MOI.Utilities.distance_to_set(dual(con_ref), dual_set)
93+
1.602881211577939e-8
94+
```
95+
96+
For the MAX-CUT problem, we know there exists a rank-1 solution where
97+
the entries of the factor are `-1` or `1` depending on the side of the cut
98+
the nodes are on. Let's now be greedy and search for a solution of rank-1.
99+
```julia
100+
julia> set_attribute(model, "maxrank", (m, n) -> 1)
101+
102+
julia> optimize!(model)
103+
104+
*** SDPLR 1.03-beta ***
105+
106+
===================================================
107+
major minor val infeas time
108+
---------------------------------------------------
109+
1 0 2.68869170e-01 8.7e-01 0
110+
2 7 8.10081717e+01 1.0e+01 0
111+
3 10 -1.81587378e+01 2.3e-01 0
112+
4 11 -1.80062673e+01 1.3e-01 0
113+
5 13 -1.79951904e+01 5.0e-02 0
114+
6 15 -1.79981240e+01 1.4e-02 0
115+
7 16 -1.79998759e+01 2.2e-03 0
116+
8 17 -1.79999980e+01 1.9e-04 0
117+
9 18 -1.80000000e+01 1.4e-05 0
118+
10 19 -1.80000000e+01 7.1e-07 0
119+
===================================================
120+
121+
DIMACS error measures: 7.12e-07 0.00e+00 0.00e+00 1.31e-05 1.59e-06 -7.81e-06
122+
123+
124+
julia> MOI.get(model, LRO.InnerAttribute(SDPLR.Factor()), VariableInSetRef(model[:dot_prod_set]))
125+
4×1 Matrix{Float64}:
126+
-0.9999998713637199
127+
0.9999995531365233
128+
0.9999997036204064
129+
0.9999995498147483
130+
```
131+
132+
Note that even though a solution of rank-1 exists, SDPLR is more likely to
133+
converge to a spurious local minimum if we use a lower-rank, so we should
134+
be careful before claiming that we found the optimal solution.
135+
Luckily, the following shows that the dual is feasible which gives a certificate
136+
of primal optimality.
137+
```julia-repl
138+
julia> MOI.Utilities.distance_to_set(dual(con_ref), dual_set)
139+
0.0
71140
```

examples/maxcut.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
using LinearAlgebra, JuMP, LowRankOpt
2+
import LowRankOpt as LRO
3+
4+
function e_i(i, n)
5+
ei = zeros(n)
6+
ei[i] = 1
7+
return ei
8+
end
29

310
function maxcut(weights, solver)
411
N = LinearAlgebra.checksquare(weights)

src/Bridges/Constraint/Constraint.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ for filename in readdir(joinpath(@__DIR__, "bridges"); join = true)
1212
include(filename)
1313
end
1414

15+
function MOI.get(
16+
model::MOI.ModelLike,
17+
attr::LRO.InnerAttribute,
18+
bridge::MOI.Bridges.Constraint.SetMapBridge,
19+
)
20+
return MOI.get(model, attr, bridge.constraint)
21+
end
22+
1523
const ConversionBridge{W,T} = MOI.Bridges.Constraint.SetConversionBridge{
1624
T,
1725
LRO.LinearCombinationInSet{

src/Bridges/Variable/Variable.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ for filename in readdir(joinpath(@__DIR__, "bridges"); join = true)
1212
include(filename)
1313
end
1414

15+
function MOI.get(
16+
model::MOI.ModelLike,
17+
attr::LRO.InnerAttribute,
18+
bridge::MOI.Bridges.Variable.SetMapBridge,
19+
)
20+
return MOI.get(model, attr, bridge.constraint)
21+
end
22+
1523
const ConversionBridge{W,T} = SetConversionBridge{
1624
T,
1725
LRO.SetDotProducts{

src/LowRankOpt.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import MathOptInterface as MOI
1111
import FillArrays
1212

1313
include("sets.jl")
14+
include("attributes.jl")
15+
include("distance_to_set.jl")
1416
include("Test/Test.jl")
1517
include("Bridges/Bridges.jl")
1618

src/attributes.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
struct InnerAttribute{A<:MOI.AbstractConstraintAttribute} <: MOI.AbstractConstraintAttribute
3+
inner::A
4+
end
5+
6+
This attributes goes through all `Bridges.Variable.SetMapBridge` and `Bridges.Constraint.SetMapBridge`, ignoring the corresponding linear transformation.
7+
"""
8+
struct InnerAttribute{A<:MOI.AbstractConstraintAttribute} <:
9+
MOI.AbstractConstraintAttribute
10+
inner::A
11+
end
12+
13+
function MOI.is_copyable(a::InnerAttribute)
14+
return MOI.is_copyable(a.inner)
15+
end
16+
17+
function MOI.is_set_by_optimize(a::InnerAttribute)
18+
return MOI.is_set_by_optimize(a.inner)
19+
end
20+
21+
function MOI.get(
22+
model::MOI.Utilities.UniversalFallback,
23+
attr::InnerAttribute,
24+
ci::MOI.ConstraintIndex,
25+
)
26+
return MOI.get(model, attr.inner, ci)
27+
end
28+
29+
function MOI.get_fallback(
30+
model::MOI.ModelLike,
31+
attr::InnerAttribute,
32+
ci::MOI.ConstraintIndex,
33+
)
34+
return MOI.get(model, attr.inner, ci)
35+
end

src/distance_to_set.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function MOI.Utilities.distance_to_set(
2+
d::MOI.Utilities.ProjectionUpperBoundDistance,
3+
x::AbstractVector{T},
4+
set::SetDotProducts{WITH_SET},
5+
) where {T,W}
6+
MOI.Utilities._check_dimension(x, set)
7+
n = length(set.vectors)
8+
vec = x[(n+1):end]
9+
init = MOI.Utilities.distance_to_set(d, vec, set.set)^2
10+
return sum(1:n; init) do i
11+
return (x[i] - MOI.Utilities.set_dot(set.vectors[i], vec, set.set))^2
12+
end
13+
end
14+
15+
function MOI.Utilities.distance_to_set(
16+
d::MOI.Utilities.ProjectionUpperBoundDistance,
17+
x::AbstractVector{T},
18+
set::LinearCombinationInSet{W},
19+
) where {T,W}
20+
MOI.Utilities._check_dimension(x, set)
21+
if W == WITH_SET
22+
init = x[(length(set.vectors)+1):end]
23+
else
24+
init = zeros(T, MOI.dimension(set.set))
25+
end
26+
y = sum(x[i] * set.vectors[i] for i in eachindex(set.vectors); init)
27+
return MOI.Utilities.distance_to_set(d, y, set.set)
28+
end

src/sets.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,26 @@ function MOI.dual_set_type(::Type{LinearCombinationInSet{W,S,V}}) where {W,S,V}
120120
return SetDotProducts{W,S,V}
121121
end
122122

123+
function MOI.Utilities.set_dot(
124+
x::AbstractVector,
125+
y::AbstractVector,
126+
set::Union{SetDotProducts{WITH_SET},LinearCombinationInSet{WITH_SET}},
127+
)
128+
n = length(set.vectors)
129+
return LinearAlgebra.dot(x[1:n], y[1:n]) +
130+
MOI.Utilities.set_dot(x[(n+1):end], y[(n+1):end], set.set)
131+
end
132+
133+
function MOI.Utilities.dot_coefficients(
134+
x::AbstractVector,
135+
set::Union{SetDotProducts{WITH_SET},LinearCombinationInSet{WITH_SET}},
136+
)
137+
c = copy(x)
138+
n = length(set.vectors)
139+
c[(n+1):end] = MOI.Utilities.dot_coefficients(x[(n+1):end], set.set)
140+
return c
141+
end
142+
123143
abstract type AbstractFactorization{T,F} <: AbstractMatrix{T} end
124144

125145
function Base.size(m::AbstractFactorization)

test/Bridges/Constraint/LinearCombinationBridge.jl

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,30 @@ function runtests()
2121
return
2222
end
2323

24+
function _model(T, model)
25+
_, cx = MOI.add_constrained_variables(
26+
model,
27+
LRO.LinearCombinationInSet{LRO.WITH_SET}(
28+
MOI.PositiveSemidefiniteConeTriangle(2),
29+
LRO.TriangleVectorization.([
30+
T[
31+
1 2
32+
2 3
33+
],
34+
T[
35+
4 5
36+
5 6
37+
],
38+
]),
39+
),
40+
)
41+
return cx
42+
end
43+
2444
function test_psd(T::Type)
2545
MOI.Bridges.runtests(
2646
LRO.Bridges.Constraint.LinearCombinationBridge,
27-
model -> begin
28-
x, _ = MOI.add_constrained_variables(
29-
model,
30-
LRO.LinearCombinationInSet{LRO.WITH_SET}(
31-
MOI.PositiveSemidefiniteConeTriangle(2),
32-
LRO.TriangleVectorization.([
33-
T[
34-
1 2
35-
2 3
36-
],
37-
T[
38-
4 5
39-
5 6
40-
],
41-
]),
42-
),
43-
)
44-
end,
47+
Base.Fix1(_model, T),
4548
model -> begin
4649
x = MOI.add_variables(model, 5)
4750
MOI.add_constraint(
@@ -61,6 +64,25 @@ function test_psd(T::Type)
6164
return
6265
end
6366

67+
struct Custom <: MOI.AbstractConstraintAttribute end
68+
MOI.is_set_by_optimize(::Custom) = false
69+
70+
function test_attribute(T::Type)
71+
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
72+
model = MOI.Bridges._bridged_model(
73+
LRO.Bridges.Constraint.LinearCombinationBridge{T},
74+
inner,
75+
)
76+
cx = _model(T, model)
77+
F = MOI.VectorAffineFunction{T}
78+
S = MOI.PositiveSemidefiniteConeTriangle
79+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
80+
MOI.set(inner, Custom(), ci, "test")
81+
@test MOI.get(inner, Custom(), ci) == "test"
82+
@test MOI.get(inner, LRO.InnerAttribute(Custom()), ci) == "test"
83+
@test MOI.get(model, LRO.InnerAttribute(Custom()), cx) == "test"
84+
end
85+
6486
end # module
6587

6688
TestConstraintLinearCombination.runtests()

test/Bridges/Variable/set_dot.jl

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,32 @@ function runtests()
2121
return
2222
end
2323

24+
function _model(T, model)
25+
x, cx = MOI.add_constrained_variables(
26+
model,
27+
LRO.SetDotProducts{LRO.WITH_SET}(
28+
MOI.PositiveSemidefiniteConeTriangle(2),
29+
LRO.TriangleVectorization.([
30+
T[
31+
1 2
32+
2 3
33+
],
34+
T[
35+
4 5
36+
5 6
37+
],
38+
]),
39+
),
40+
)
41+
MOI.add_constraint(model, one(T) * x[1], MOI.EqualTo(zero(T)))
42+
MOI.add_constraint(model, one(T) * x[2], MOI.LessThan(zero(T)))
43+
return cx
44+
end
45+
2446
function test_psd(T::Type)
2547
MOI.Bridges.runtests(
2648
LRO.Bridges.Variable.DotProductsBridge,
27-
model -> begin
28-
x, _ = MOI.add_constrained_variables(
29-
model,
30-
LRO.SetDotProducts{LRO.WITH_SET}(
31-
MOI.PositiveSemidefiniteConeTriangle(2),
32-
LRO.TriangleVectorization.([
33-
T[
34-
1 2
35-
2 3
36-
],
37-
T[
38-
4 5
39-
5 6
40-
],
41-
]),
42-
),
43-
)
44-
MOI.add_constraint(model, one(T) * x[1], MOI.EqualTo(zero(T)))
45-
MOI.add_constraint(model, one(T) * x[2], MOI.LessThan(zero(T)))
46-
end,
49+
Base.Fix1(_model, T),
4750
model -> begin
4851
Q, _ = MOI.add_constrained_variables(
4952
model,
@@ -66,6 +69,43 @@ function test_psd(T::Type)
6669
return
6770
end
6871

72+
struct Custom <: MOI.AbstractConstraintAttribute
73+
is_copyable::Bool
74+
is_set_by_optimize::Bool
75+
end
76+
MOI.is_copyable(c::Custom) = c.is_copyable
77+
MOI.is_set_by_optimize(c::Custom) = c.is_set_by_optimize
78+
79+
function test_attribute(T::Type)
80+
inner = MOI.Utilities.UniversalFallback(MOI.Utilities.Model{T}())
81+
model = MOI.Bridges._bridged_model(
82+
LRO.Bridges.Variable.DotProductsBridge{T},
83+
inner,
84+
)
85+
cx = _model(T, model)
86+
F = MOI.VectorOfVariables
87+
S = MOI.PositiveSemidefiniteConeTriangle
88+
ci = only(MOI.get(inner, MOI.ListOfConstraintIndices{F,S}()))
89+
attr = Custom(true, false)
90+
MOI.set(inner, attr, ci, "test")
91+
@test MOI.get(inner, attr, ci) == "test"
92+
attr = LRO.InnerAttribute(attr)
93+
@test MOI.get(inner, attr, ci) == "test"
94+
@test MOI.get(model, attr, cx) == "test"
95+
for is_copyable in [false, true]
96+
for is_set_by_optimize in [false, true]
97+
attr = LRO.InnerAttribute(Custom(is_copyable, is_set_by_optimize))
98+
@test MOI.is_copyable(attr) == is_copyable
99+
@test MOI.is_set_by_optimize(attr) == is_set_by_optimize
100+
end
101+
end
102+
@test_throws MOI.GetAttributeNotAllowed{Custom} MOI.get(
103+
inner.model,
104+
attr,
105+
ci,
106+
)
107+
end
108+
69109
end # module
70110

71111
TestVariableDotProducts.runtests()

0 commit comments

Comments
 (0)