Skip to content

Commit 92f73ae

Browse files
authored
Add VectorNonlinearFunction (#2201)
1 parent 5c1515b commit 92f73ae

File tree

21 files changed

+979
-77
lines changed

21 files changed

+979
-77
lines changed

docs/src/manual/standard_form.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ The function types implemented in MathOptInterface.jl are:
4141
| [`VectorAffineFunction`](@ref) | ``A x + b``, where ``A`` is a matrix and ``b`` is a vector. |
4242
| [`ScalarQuadraticFunction`](@ref) | ``\frac{1}{2} x^T Q x + a^T x + b``, where ``Q`` is a symmetric matrix, ``a`` is a vector, and ``b`` is a constant. |
4343
| [`VectorQuadraticFunction`](@ref) | A vector of scalar-valued quadratic functions. |
44+
| [`VectorNonlinearFunction`](@ref) | ``f(x)``, where ``f`` is a vector-valued nonlinear function. |
4445

4546
Extensions for nonlinear programming are present but not yet well documented.
4647

docs/src/reference/standard_form.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ VectorAffineTerm
3737
VectorAffineFunction
3838
VectorQuadraticTerm
3939
VectorQuadraticFunction
40+
VectorNonlinearFunction
4041
```
4142

4243
## Sets

src/Bridges/Constraint/bridges/square.jl

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,58 @@ _square_offset(::MOI.AbstractSymmetricMatrixSetSquare) = Int[]
7676
_square_offset(::MOI.RootDetConeSquare) = Int[1]
7777
_square_offset(::MOI.LogDetConeSquare) = Int[1, 2]
7878

79+
function _constrain_off_diagonals(
80+
model::MOI.ModelLike,
81+
::Type{T},
82+
::Tuple{Int,Int},
83+
f_ij::F,
84+
f_ji::F,
85+
) where {T,F<:MOI.ScalarNonlinearFunction}
86+
if isapprox(f_ij, f_ji)
87+
return nothing
88+
end
89+
return MOI.Utilities.normalize_and_add_constraint(
90+
model,
91+
MOI.ScalarNonlinearFunction(:-, Any[f_ij, f_ji]),
92+
MOI.EqualTo(zero(T));
93+
allow_modify_function = true,
94+
)
95+
end
96+
97+
function _constrain_off_diagonals(
98+
model::MOI.ModelLike,
99+
::Type{T},
100+
ij::Tuple{Int,Int},
101+
f_ij::F,
102+
f_ji::F,
103+
) where {T,F}
104+
diff = MOI.Utilities.operate!(-, T, f_ij, f_ji)
105+
MOI.Utilities.canonicalize!(diff)
106+
# The value 1e-10 was decided in https://github.com/jump-dev/JuMP.jl/pull/976
107+
# This avoid generating symmetrization constraints when the
108+
# functions at entries (i, j) and (j, i) are almost identical
109+
if MOI.Utilities.isapprox_zero(diff, 1e-10)
110+
return nothing
111+
end
112+
if MOI.Utilities.isapprox_zero(diff, 1e-8)
113+
i, j = ij
114+
@warn(
115+
"The entries ($i, $j) and ($j, $i) of the matrix are " *
116+
"almost identical, but a constraint has been added " *
117+
"to ensure their equality because the largest " *
118+
"difference between the coefficients is smaller than " *
119+
"1e-8 but larger than 1e-10. This usually means that " *
120+
"there is a modeling error in your formulation.",
121+
)
122+
end
123+
return MOI.Utilities.normalize_and_add_constraint(
124+
model,
125+
diff,
126+
MOI.EqualTo(zero(T));
127+
allow_modify_function = true,
128+
)
129+
end
130+
79131
function bridge_constraint(
80132
::Type{SquareBridge{T,F,G,TT,ST}},
81133
model::MOI.ModelLike,
@@ -93,32 +145,14 @@ function bridge_constraint(
93145
for i in 1:j
94146
k += 1
95147
push!(upper_triangle_indices, k)
96-
# We constrain the entries (i, j) and (j, i) to be equal
97-
f_ij = f_scalars[offset+i+(j-1)*dim]
98-
f_ji = f_scalars[offset+j+(i-1)*dim]
99-
diff = MOI.Utilities.operate!(-, T, f_ij, f_ji)
100-
MOI.Utilities.canonicalize!(diff)
101-
# The value 1e-10 was decided in https://github.com/jump-dev/JuMP.jl/pull/976
102-
# This avoid generating symmetrization constraints when the
103-
# functions at entries (i, j) and (j, i) are almost identical
104-
if !MOI.Utilities.isapprox_zero(diff, 1e-10)
105-
if MOI.Utilities.isapprox_zero(diff, 1e-8)
106-
@warn(
107-
"The entries ($i, $j) and ($j, $i) of the matrix are " *
108-
"almost identical, but a constraint has been added " *
109-
"to ensure their equality because the largest " *
110-
"difference between the coefficients is smaller than " *
111-
"1e-8 but larger than 1e-10. This usually means that " *
112-
"there is a modeling error in your formulation.",
113-
)
148+
if i !== j
149+
# We constrain the entries (i, j) and (j, i) to be equal
150+
f_ij = f_scalars[offset+i+(j-1)*dim]
151+
f_ji = f_scalars[offset+j+(i-1)*dim]
152+
ci = _constrain_off_diagonals(model, T, (i, j), f_ij, f_ji)
153+
if ci !== nothing
154+
push!(sym, (i, j) => ci)
114155
end
115-
ci = MOI.Utilities.normalize_and_add_constraint(
116-
model,
117-
diff,
118-
MOI.EqualTo(zero(T));
119-
allow_modify_function = true,
120-
)
121-
push!(sym, (i, j) => ci)
122156
end
123157
end
124158
k += dim - j

src/Bridges/Constraint/bridges/vectorize.jl

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,6 @@ function MOI.supports_constraint(
6767
return true
6868
end
6969

70-
function MOI.supports_constraint(
71-
::Type{VectorizeBridge{T}},
72-
::Type{MOI.ScalarNonlinearFunction},
73-
::Type{<:MOI.Utilities.ScalarLinearSet{T}},
74-
) where {T}
75-
return false
76-
end
77-
7870
function MOI.Bridges.added_constrained_variable_types(::Type{<:VectorizeBridge})
7971
return Tuple{Type}[]
8072
end

src/Test/test_basic_constraint.jl

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ function _function(
7777
)
7878
end
7979

80+
function _function(
81+
::Type{T},
82+
::Type{MOI.VectorNonlinearFunction},
83+
x::Vector{MOI.VariableIndex},
84+
) where {T}
85+
f = MOI.ScalarNonlinearFunction(
86+
:+,
87+
Any[MOI.ScalarNonlinearFunction(:^, Any[xi, 2]) for xi in x],
88+
)
89+
return MOI.VectorNonlinearFunction([f; x])
90+
end
91+
8092
# Default fallback.
8193
_set(::Any, ::Type{S}) where {S} = _set(S)
8294

@@ -334,7 +346,12 @@ for s in [
334346
:ScalarNonlinearFunction,
335347
)
336348
else
337-
(:VectorOfVariables, :VectorAffineFunction, :VectorQuadraticFunction)
349+
(
350+
:VectorOfVariables,
351+
:VectorAffineFunction,
352+
:VectorQuadraticFunction,
353+
:VectorNonlinearFunction,
354+
)
338355
end
339356
for f in functions
340357
func = Symbol("test_basic_$(f)_$(s)")

src/Test/test_multiobjective.jl

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,82 @@ function test_multiobjective_vector_quadratic_function_delete_vector(
228228
@test MOI.get(model, MOI.ObjectiveFunction{F}()) new_f
229229
return
230230
end
231+
232+
function test_multiobjective_vector_nonlinear(
233+
model::MOI.ModelLike,
234+
::Config{T},
235+
) where {T}
236+
F = MOI.VectorNonlinearFunction
237+
@requires MOI.supports(model, MOI.ObjectiveFunction{F}())
238+
x = MOI.add_variables(model, 2)
239+
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
240+
f = MOI.VectorNonlinearFunction(
241+
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
242+
) # [x[1]^2, x[2]]
243+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
244+
MOI.set(model, MOI.ObjectiveFunction{F}(), f)
245+
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
246+
@test MOI.get(model, MOI.ObjectiveFunction{F}()) f
247+
return
248+
end
249+
250+
function test_multiobjective_vector_nonlinear_delete(
251+
model::MOI.ModelLike,
252+
::Config{T},
253+
) where {T}
254+
F = MOI.VectorNonlinearFunction
255+
@requires MOI.supports(model, MOI.ObjectiveFunction{F}())
256+
x = MOI.add_variables(model, 2)
257+
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
258+
f = MOI.VectorNonlinearFunction(
259+
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
260+
) # [x[1]^2, x[2]]
261+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
262+
MOI.set(model, MOI.ObjectiveFunction{F}(), f)
263+
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
264+
@test MOI.get(model, MOI.ObjectiveFunction{F}()) f
265+
@test_throws MOI.DeleteNotAllowed MOI.delete(model, x[1])
266+
return
267+
end
268+
269+
function test_multiobjective_vector_nonlinear_delete_vector(
270+
model::MOI.ModelLike,
271+
::Config{T},
272+
) where {T}
273+
F = MOI.VectorNonlinearFunction
274+
@requires MOI.supports(model, MOI.ObjectiveFunction{F}())
275+
x = MOI.add_variables(model, 2)
276+
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
277+
f = MOI.VectorNonlinearFunction(
278+
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
279+
) # [x[1]^2, x[2]]
280+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
281+
MOI.set(model, MOI.ObjectiveFunction{F}(), f)
282+
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
283+
@test MOI.get(model, MOI.ObjectiveFunction{F}()) f
284+
@test_throws MOI.DeleteNotAllowed MOI.delete(model, x)
285+
return
286+
end
287+
288+
function test_multiobjective_vector_nonlinear_modify(
289+
model::MOI.ModelLike,
290+
::Config{T},
291+
) where {T}
292+
F = MOI.VectorNonlinearFunction
293+
attr = MOI.ObjectiveFunction{F}()
294+
@requires MOI.supports(model, attr)
295+
x = MOI.add_variables(model, 2)
296+
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
297+
f = MOI.VectorNonlinearFunction(
298+
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
299+
) # [x[1]^2, x[2]]
300+
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
301+
MOI.set(model, attr, f)
302+
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
303+
@test MOI.get(model, attr) f
304+
@test_throws(
305+
MOI.ModifyObjectiveNotAllowed,
306+
MOI.modify(model, attr, MOI.VectorConstantChange(T[1, 2])),
307+
)
308+
return
309+
end

src/Test/test_nonlinear.jl

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,3 +1673,93 @@ function setup_test(
16731673
model.eval_objective_value = obj_flag
16741674
end
16751675
end
1676+
1677+
function test_nonlinear_vector_complements(
1678+
model::MOI.ModelLike,
1679+
config::MOI.Test.Config{T},
1680+
) where {T}
1681+
@requires T == Float64
1682+
@requires _supports(config, MOI.optimize!)
1683+
F = MOI.ScalarNonlinearFunction
1684+
@requires MOI.supports_constraint(model, F, MOI.Complements)
1685+
x = MOI.add_variables(model, 4)
1686+
MOI.add_constraint.(model, x, MOI.Interval(T(0), T(10)))
1687+
MOI.set.(model, MOI.VariablePrimalStart(), x, T(1))
1688+
# f = [
1689+
# -1 * x3^2 + -1 * x4 + 2.0
1690+
# x3^3 + -1.0 * 2x4^2 + 2.0
1691+
# x1^5 + -1.0 * x2 + 2.0 * x3 + -2.0 * x4 + -2.0
1692+
# x1 + 2.0 * x2^3 + -2.0 * x3 + 4.0 * x4 + -6.0
1693+
# x...
1694+
# ]
1695+
f = MOI.VectorNonlinearFunction([
1696+
MOI.ScalarNonlinearFunction(
1697+
:+,
1698+
Any[
1699+
MOI.ScalarNonlinearFunction(:*, Any[-T(1), x[3], x[3]]),
1700+
MOI.ScalarNonlinearFunction(:*, Any[-T(1), x[4]]),
1701+
T(2),
1702+
],
1703+
),
1704+
MOI.ScalarNonlinearFunction(
1705+
:+,
1706+
Any[
1707+
MOI.ScalarNonlinearFunction(:^, Any[x[3], 3]),
1708+
MOI.ScalarNonlinearFunction(:*, Any[-T(2), x[4], x[4]]),
1709+
T(2),
1710+
],
1711+
),
1712+
MOI.ScalarNonlinearFunction(
1713+
:+,
1714+
Any[
1715+
MOI.ScalarNonlinearFunction(:^, Any[x[1], 5]),
1716+
MOI.ScalarAffineFunction{T}(
1717+
MOI.ScalarAffineTerm.(T[-1, 2, -2], x[2:4]),
1718+
-T(2),
1719+
),
1720+
],
1721+
),
1722+
MOI.ScalarNonlinearFunction(
1723+
:+,
1724+
Any[
1725+
MOI.ScalarNonlinearFunction(:*, Any[T(2), x[2], x[2], x[2]]),
1726+
MOI.ScalarAffineFunction{T}(
1727+
MOI.ScalarAffineTerm.(T[1, -2, 4], [x[1], x[3], x[4]]),
1728+
-T(6),
1729+
),
1730+
],
1731+
),
1732+
x[1],
1733+
x[2],
1734+
x[3],
1735+
x[4],
1736+
])
1737+
MOI.add_constraint(model, f, MOI.Complements(8))
1738+
MOI.optimize!(model)
1739+
@test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
1740+
@test (
1741+
MOI.get.(model, MOI.VariablePrimal(), x),
1742+
T[1.2847523, 0.9729165, 0.9093762, 1.1730350],
1743+
config,
1744+
)
1745+
return
1746+
end
1747+
1748+
function setup_test(
1749+
::typeof(test_nonlinear_vector_complements),
1750+
model::MOIU.MockOptimizer,
1751+
config::Config{T},
1752+
) where {T}
1753+
if T != Float64
1754+
return # Skip for non-Float64 solvers
1755+
end
1756+
MOI.Utilities.set_mock_optimize!(
1757+
model,
1758+
mock -> MOI.Utilities.mock_optimize!(
1759+
mock,
1760+
config.optimal_status,
1761+
T[1.2847523, 0.9729165, 0.9093762, 1.1730350],
1762+
),
1763+
)
1764+
return
1765+
end

0 commit comments

Comments
 (0)