Skip to content

Commit f07cb1c

Browse files
authored
Merge pull request #1045 from chriscoey/fixbridgeconstants
add L1/Linf cone tests and fix bridge when VAF has nonzero constants
2 parents 1315e31 + 1cb4f50 commit f07cb1c

File tree

4 files changed

+472
-173
lines changed

4 files changed

+472
-173
lines changed

src/Bridges/Constraint/norm_to_lp.jl

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -70,69 +70,61 @@ The `NormOneCone` is representable with LP constraints, since
7070
``t \\ge \\sum_i \\lvert x_i \\rvert`` if and only if there exists a vector y such that
7171
``t \\ge \\sum_i y_i`` and ``y_i \\ge x_i``, ``y_i \\ge -x_i`` for all ``i``.
7272
"""
73-
struct NormOneBridge{T, F, G, H} <: AbstractBridge
73+
struct NormOneBridge{T, F, G} <: AbstractBridge
7474
y::Vector{MOI.VariableIndex}
75-
ge_index::CI{F, MOI.GreaterThan{T}}
76-
nn_index::CI{G, MOI.Nonnegatives}
75+
nn_index::CI{F, MOI.Nonnegatives}
7776
end
78-
function bridge_constraint(::Type{NormOneBridge{T, F, G, H}}, model::MOI.ModelLike, f::H, s::MOI.NormOneCone) where {T, F, G, H}
77+
function bridge_constraint(::Type{NormOneBridge{T, F, G}}, model::MOI.ModelLike, f::G, s::MOI.NormOneCone) where {T, F, G}
7978
f_scalars = MOIU.eachscalar(f)
8079
d = MOI.dimension(s)
8180
y = MOI.add_variables(model, d - 1)
82-
ge_index = MOIU.normalize_and_add_constraint(model, MOIU.operate(-, T, f_scalars[1], MOIU.operate(sum, T, y)), MOI.GreaterThan(zero(T)), allow_modify_function=true)
81+
ge = MOIU.operate(-, T, f_scalars[1], MOIU.operate(sum, T, y))
8382
lb = f_scalars[2:d]
8483
ub = MOIU.operate(-, T, lb)
8584
lb = MOIU.operate!(+, T, lb, MOI.VectorOfVariables(y))
8685
ub = MOIU.operate!(+, T, ub, MOI.VectorOfVariables(y))
87-
f_new = MOIU.operate(vcat, T, ub, lb)
88-
nn_index = MOI.add_constraint(model, f_new, MOI.Nonnegatives(2d - 2))
89-
return NormOneBridge{T, F, G, H}(y, ge_index, nn_index)
86+
f_new = MOIU.operate(vcat, T, ge, ub, lb)
87+
nn_index = MOI.add_constraint(model, f_new, MOI.Nonnegatives(2d - 1))
88+
return NormOneBridge{T, F, G}(y, nn_index)
9089
end
9190

9291
MOI.supports_constraint(::Type{NormOneBridge{T}}, ::Type{<:MOI.AbstractVectorFunction}, ::Type{MOI.NormOneCone}) where T = true
9392
MOIB.added_constrained_variable_types(::Type{<:NormOneBridge}) = Tuple{DataType}[]
94-
MOIB.added_constraint_types(::Type{NormOneBridge{T, F, G, H}}) where {T, F, G, H} = [(F, MOI.GreaterThan{T}), (G, MOI.Nonnegatives)]
95-
function concrete_bridge_type(::Type{<:NormOneBridge{T}}, H::Type{<:MOI.AbstractVectorFunction}, ::Type{MOI.NormOneCone}) where T
96-
S = MOIU.scalar_type(H)
97-
F = MOIU.promote_operation(+, T, S, S)
98-
G = MOIU.promote_operation(+, T, H, H)
99-
return NormOneBridge{T, F, G, H}
93+
MOIB.added_constraint_types(::Type{<:NormOneBridge{T, F}}) where {T, F} = [(F, MOI.Nonnegatives)]
94+
function concrete_bridge_type(::Type{<:NormOneBridge{T}}, G::Type{<:MOI.AbstractVectorFunction}, ::Type{MOI.NormOneCone}) where T
95+
S = MOIU.scalar_type(G)
96+
F = MOIU.promote_operation(vcat, T, MOIU.promote_operation(+, T, S, S), MOIU.promote_operation(-, T, S, S))
97+
return NormOneBridge{T, F, G}
10098
end
10199

102100
# Attributes, Bridge acting as a model
103101
MOI.get(b::NormOneBridge, ::MOI.NumberOfVariables) = length(b.y)
104102
MOI.get(b::NormOneBridge, ::MOI.ListOfVariableIndices) = b.y
105-
MOI.get(b::NormOneBridge{T, F, G, H}, ::MOI.NumberOfConstraints{F, MOI.GreaterThan{T}}) where {T, F, G, H} = 1
106-
MOI.get(b::NormOneBridge{T, F, G, H}, ::MOI.NumberOfConstraints{G, MOI.Nonnegatives}) where {T, F, G, H} = 1
107-
MOI.get(b::NormOneBridge{T, F, G, H}, ::MOI.ListOfConstraintIndices{F, MOI.GreaterThan{T}}) where {T, F, G, H} = [b.ge_index]
108-
MOI.get(b::NormOneBridge{T, F, G, H}, ::MOI.ListOfConstraintIndices{G, MOI.Nonnegatives}) where {T, F, G, H} = [b.nn_index]
103+
MOI.get(b::NormOneBridge{T, F}, ::MOI.NumberOfConstraints{F, MOI.Nonnegatives}) where {T, F} = 1
104+
MOI.get(b::NormOneBridge{T, F}, ::MOI.ListOfConstraintIndices{F, MOI.Nonnegatives}) where {T, F} = [b.nn_index]
109105

110106
# References
111107
function MOI.delete(model::MOI.ModelLike, c::NormOneBridge)
112108
MOI.delete(model, c.nn_index)
113-
MOI.delete(model, c.ge_index)
114109
MOI.delete(model, c.y)
115110
end
116111

117112
# Attributes, Bridge acting as a constraint
118-
function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintFunction, c::NormOneBridge{T, F, G, H}) where {T, F, G, H}
119-
ge_func = MOI.get(model, MOI.ConstraintFunction(), c.ge_index)
113+
function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintFunction, c::NormOneBridge{T, F, G}) where {T, F, G}
120114
nn_func = MOIU.eachscalar(MOI.get(model, MOI.ConstraintFunction(), c.nn_index))
121-
t = MOIU.operate!(+, T, ge_func, MOIU.operate!(/, T, sum(nn_func), T(2)))
122-
d = div(length(nn_func), 2)
123-
x = MOIU.operate!(/, T, MOIU.operate!(-, T, nn_func[(d + 1):end], nn_func[1:d]), T(2))
124-
return MOIU.convert_approx(H, MOIU.remove_variable(MOIU.operate(vcat, T, t, x), c.y))
115+
t = MOIU.operate!(/, T, nn_func[1] + sum(nn_func), T(2))
116+
d = div(length(nn_func) - 1, 2)
117+
x = MOIU.operate!(/, T, MOIU.operate!(-, T, nn_func[(d + 2):end], nn_func[2:(d + 1)]), T(2))
118+
return MOIU.convert_approx(G, MOIU.remove_variable(MOIU.operate(vcat, T, t, x), c.y))
125119
end
126120
function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintSet, c::NormOneBridge)
127-
dim = 1 + div(MOI.dimension(MOI.get(model, MOI.ConstraintSet(), c.nn_index)), 2)
121+
dim = div(MOI.dimension(MOI.get(model, MOI.ConstraintSet(), c.nn_index)) + 1, 2)
128122
return MOI.NormOneCone(dim)
129123
end
130-
131124
function MOI.supports(
132125
::MOI.ModelLike,
133126
::Union{MOI.ConstraintPrimalStart, MOI.ConstraintDualStart},
134127
::Type{<:NormOneBridge})
135-
136128
return true
137129
end
138130
function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintPrimalStart,
@@ -142,18 +134,17 @@ function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintPrimalStart,
142134
for i in eachindex(bridge.y)
143135
MOI.set(model, MOI.VariablePrimalStart(), bridge.y[i], y_value[i])
144136
end
145-
MOI.set(model, attr, bridge.nn_index, [y_value - x_value; y_value + x_value])
146-
MOI.set(model, attr, bridge.ge_index, value[1] - reduce(+, y_value, init=zero(T)))
137+
nn_value = vcat(value[1] - reduce(+, y_value, init=zero(T)), y_value - x_value, y_value + x_value)
138+
MOI.set(model, attr, bridge.nn_index, nn_value)
147139
return
148140
end
149141
function MOI.get(model::MOI.ModelLike,
150142
attr::Union{MOI.ConstraintPrimal, MOI.ConstraintPrimalStart},
151143
bridge::NormOneBridge)
152-
ge_primal = MOI.get(model, attr, bridge.ge_index)
153144
nn_primal = MOI.get(model, attr, bridge.nn_index)
154-
t = ge_primal + sum(nn_primal) / 2
145+
t = (nn_primal[1] + sum(nn_primal)) / 2
155146
d = length(bridge.y)
156-
x = (nn_primal[(d + 1):end] - nn_primal[1:d]) / 2
147+
x = (nn_primal[(d + 2):end] - nn_primal[2:(d + 1)]) / 2
157148
return vcat(t, x)
158149
end
159150
# Given a_i is dual on y_i - x_i >= 0 and b_i is dual on y_i + x_i >= 0 and c is dual on t - sum(y) >= 0,
@@ -162,27 +153,26 @@ end
162153
function MOI.get(model::MOI.ModelLike,
163154
attr::Union{MOI.ConstraintDual, MOI.ConstraintDualStart},
164155
bridge::NormOneBridge)
165-
t = MOI.get(model, attr, bridge.ge_index)
166156
nn_dual = MOI.get(model, attr, bridge.nn_index)
167157
d = length(bridge.y)
168-
x = nn_dual[(d + 1):end] - nn_dual[1:d]
169-
return vcat(t, x)
158+
x = nn_dual[(d + 2):end] - nn_dual[2:(d + 1)]
159+
return vcat(nn_dual[1], x)
170160
end
171-
# value[1 + i] = nn_dual[d + i] - nn_dual[i]
161+
# value[1 + i] = nn_dual[1 + d + i] - nn_dual[1 + i]
172162
# and `nn_dual` is nonnegative. By complementarity slackness, only one of each
173163
# `nn_dual` can be nonzero (except if `x = 0`) so we can set
174164
# depending on the sense of `value[1 + i]`.
175165
function MOI.set(model::MOI.ModelLike, ::MOI.ConstraintDualStart,
176166
bridge::NormOneBridge, value)
177-
t = MOI.set(model, MOI.ConstraintDualStart(), bridge.ge_index, value[1])
178167
d = length(bridge.y)
179-
nn_dual = zeros(eltype(value), 2d)
168+
nn_dual = zeros(eltype(value), 2d + 1)
169+
nn_dual[1] = value[1]
180170
for i in eachindex(bridge.y)
181171
v = value[1 + i]
182172
if v < 0
183-
nn_dual[i] = -v
173+
nn_dual[1 + i] = -v
184174
else
185-
nn_dual[d + i] = v
175+
nn_dual[1 + d + i] = v
186176
end
187177
end
188178
MOI.set(model, MOI.ConstraintDualStart(), bridge.nn_index, nn_dual)

src/Test/contconic.jl

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,9 +432,77 @@ function norminf2test(model::MOI.ModelLike, config::TestConfig)
432432
end
433433
end
434434

435+
function norminf3test(model::MOI.ModelLike, config::TestConfig)
436+
atol = config.atol
437+
rtol = config.rtol
438+
# Problem NormInf3
439+
# min x
440+
# st (-1 + x, 2 .+ y) in NormInf(1 + n)
441+
# (1 .+ y) in Nonnegatives(n)
442+
# let n = 3. optimal solution: y .= -1, x = 2
443+
444+
@test MOIU.supports_default_copy_to(model, #=copy_names=# false)
445+
@test MOI.supports(model, MOI.ObjectiveFunction{MOI.SingleVariable}())
446+
@test MOI.supports(model, MOI.ObjectiveSense())
447+
@test MOI.supports_constraint(model, MOI.VectorAffineFunction{Float64}, MOI.NormInfinityCone)
448+
449+
MOI.empty!(model)
450+
@test MOI.is_empty(model)
451+
452+
x = MOI.add_variable(model)
453+
y = MOI.add_variables(model, 3)
454+
455+
MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x))
456+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
457+
458+
norminf_vaf = MOI.VectorAffineFunction(MOI.VectorAffineTerm.(1:4,
459+
MOI.ScalarAffineTerm.(1.0, vcat(x, y))), [-1.0, 2, 2, 2])
460+
norminf = MOI.add_constraint(model, norminf_vaf, MOI.NormInfinityCone(4))
461+
nonneg_vaf = MOI.VectorAffineFunction(MOI.VectorAffineTerm.(1:3,
462+
MOI.ScalarAffineTerm.(1.0, y)), ones(3))
463+
nonneg = MOI.add_constraint(model, nonneg_vaf, MOI.Nonnegatives(3))
464+
465+
@test MOI.get(model, MOI.NumberOfConstraints{MOI.VectorAffineFunction{Float64}, MOI.NormInfinityCone}()) == 1
466+
@test MOI.get(model, MOI.NumberOfConstraints{MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives}()) == 1
467+
loc = MOI.get(model, MOI.ListOfConstraints())
468+
@test length(loc) == 2
469+
@test (MOI.VectorAffineFunction{Float64}, MOI.NormInfinityCone) in loc
470+
@test (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) in loc
471+
472+
if config.solve
473+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
474+
475+
MOI.optimize!(model)
476+
477+
@test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
478+
479+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
480+
if config.duals
481+
@test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
482+
end
483+
484+
@test MOI.get(model, MOI.ObjectiveValue()) 2 atol=atol rtol=rtol
485+
if config.dual_objective_value
486+
@test MOI.get(model, MOI.DualObjectiveValue()) 2 atol=atol rtol=rtol
487+
end
488+
489+
@test MOI.get(model, MOI.VariablePrimal(), x) 2 atol=atol rtol=rtol
490+
@test MOI.get(model, MOI.VariablePrimal(), y) fill(-1.0, 3) atol=atol rtol=rtol
491+
492+
@test MOI.get(model, MOI.ConstraintPrimal(), norminf) ones(4) atol=atol rtol=rtol
493+
@test MOI.get(model, MOI.ConstraintPrimal(), nonneg) zeros(3) atol=atol rtol=rtol
494+
495+
if config.duals
496+
@test MOI.get(model, MOI.ConstraintDual(), norminf) vcat(1, fill(-inv(3), 3)) atol=atol rtol=rtol
497+
@test MOI.get(model, MOI.ConstraintDual(), nonneg) fill(inv(3), 3) atol=atol rtol=rtol
498+
end
499+
end
500+
end
501+
435502
const norminftests = Dict("norminf1v" => norminf1vtest,
436503
"norminf1f" => norminf1ftest,
437-
"norminf2" => norminf2test)
504+
"norminf2" => norminf2test,
505+
"norminf3" => norminf3test)
438506

439507
@moitestset norminf
440508

@@ -568,9 +636,77 @@ function normone2test(model::MOI.ModelLike, config::TestConfig)
568636
end
569637
end
570638

639+
function normone3test(model::MOI.ModelLike, config::TestConfig)
640+
atol = config.atol
641+
rtol = config.rtol
642+
# Problem NormOne3
643+
# min x
644+
# st (-1 + x, 2 .+ y) in NormOne(1 + n)
645+
# (1 .+ y) in Nonnegatives(n)
646+
# let n = 3. optimal solution: y .= -1, x = 4
647+
648+
@test MOIU.supports_default_copy_to(model, #=copy_names=# false)
649+
@test MOI.supports(model, MOI.ObjectiveFunction{MOI.SingleVariable}())
650+
@test MOI.supports(model, MOI.ObjectiveSense())
651+
@test MOI.supports_constraint(model, MOI.VectorAffineFunction{Float64}, MOI.NormOneCone)
652+
653+
MOI.empty!(model)
654+
@test MOI.is_empty(model)
655+
656+
x = MOI.add_variable(model)
657+
y = MOI.add_variables(model, 3)
658+
659+
MOI.set(model, MOI.ObjectiveFunction{MOI.SingleVariable}(), MOI.SingleVariable(x))
660+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
661+
662+
norminf_vaf = MOI.VectorAffineFunction(MOI.VectorAffineTerm.(1:4,
663+
MOI.ScalarAffineTerm.(1.0, vcat(x, y))), [-1.0, 2, 2, 2])
664+
norminf = MOI.add_constraint(model, norminf_vaf, MOI.NormOneCone(4))
665+
nonneg_vaf = MOI.VectorAffineFunction(MOI.VectorAffineTerm.(1:3,
666+
MOI.ScalarAffineTerm.(1.0, y)), ones(3))
667+
nonneg = MOI.add_constraint(model, nonneg_vaf, MOI.Nonnegatives(3))
668+
669+
@test MOI.get(model, MOI.NumberOfConstraints{MOI.VectorAffineFunction{Float64}, MOI.NormOneCone}()) == 1
670+
@test MOI.get(model, MOI.NumberOfConstraints{MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives}()) == 1
671+
loc = MOI.get(model, MOI.ListOfConstraints())
672+
@test length(loc) == 2
673+
@test (MOI.VectorAffineFunction{Float64}, MOI.NormOneCone) in loc
674+
@test (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) in loc
675+
676+
if config.solve
677+
@test MOI.get(model, MOI.TerminationStatus()) == MOI.OPTIMIZE_NOT_CALLED
678+
679+
MOI.optimize!(model)
680+
681+
@test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
682+
683+
@test MOI.get(model, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT
684+
if config.duals
685+
@test MOI.get(model, MOI.DualStatus()) == MOI.FEASIBLE_POINT
686+
end
687+
688+
@test MOI.get(model, MOI.ObjectiveValue()) 4 atol=atol rtol=rtol
689+
if config.dual_objective_value
690+
@test MOI.get(model, MOI.DualObjectiveValue()) 4 atol=atol rtol=rtol
691+
end
692+
693+
@test MOI.get(model, MOI.VariablePrimal(), x) 4 atol=atol rtol=rtol
694+
@test MOI.get(model, MOI.VariablePrimal(), y) fill(-1.0, 3) atol=atol rtol=rtol
695+
696+
@test MOI.get(model, MOI.ConstraintPrimal(), norminf) vcat(3, ones(3)) atol=atol rtol=rtol
697+
@test MOI.get(model, MOI.ConstraintPrimal(), nonneg) zeros(3) atol=atol rtol=rtol
698+
699+
if config.duals
700+
@test MOI.get(model, MOI.ConstraintDual(), norminf) vcat(1, fill(-1, 3)) atol=atol rtol=rtol
701+
@test MOI.get(model, MOI.ConstraintDual(), nonneg) ones(3) atol=atol rtol=rtol
702+
end
703+
end
704+
end
705+
571706
const normonetests = Dict("normone1v" => normone1vtest,
572707
"normone1f" => normone1ftest,
573-
"normone2" => normone2test)
708+
"normone2" => normone2test,
709+
"normone3" => normone3test)
574710

575711
@moitestset normone
576712

0 commit comments

Comments
 (0)