Skip to content

Commit

Permalink
Add VectorNonlinearFunction (#2201)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Aug 14, 2023
1 parent 5c1515b commit 92f73ae
Show file tree
Hide file tree
Showing 21 changed files with 979 additions and 77 deletions.
1 change: 1 addition & 0 deletions docs/src/manual/standard_form.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The function types implemented in MathOptInterface.jl are:
| [`VectorAffineFunction`](@ref) | ``A x + b``, where ``A`` is a matrix and ``b`` is a vector. |
| [`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. |
| [`VectorQuadraticFunction`](@ref) | A vector of scalar-valued quadratic functions. |
| [`VectorNonlinearFunction`](@ref) | ``f(x)``, where ``f`` is a vector-valued nonlinear function. |

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

Expand Down
1 change: 1 addition & 0 deletions docs/src/reference/standard_form.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ VectorAffineTerm
VectorAffineFunction
VectorQuadraticTerm
VectorQuadraticFunction
VectorNonlinearFunction
```

## Sets
Expand Down
84 changes: 59 additions & 25 deletions src/Bridges/Constraint/bridges/square.jl
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@ _square_offset(::MOI.AbstractSymmetricMatrixSetSquare) = Int[]
_square_offset(::MOI.RootDetConeSquare) = Int[1]
_square_offset(::MOI.LogDetConeSquare) = Int[1, 2]

function _constrain_off_diagonals(
model::MOI.ModelLike,
::Type{T},
::Tuple{Int,Int},
f_ij::F,
f_ji::F,
) where {T,F<:MOI.ScalarNonlinearFunction}
if isapprox(f_ij, f_ji)
return nothing
end
return MOI.Utilities.normalize_and_add_constraint(
model,
MOI.ScalarNonlinearFunction(:-, Any[f_ij, f_ji]),
MOI.EqualTo(zero(T));
allow_modify_function = true,
)
end

function _constrain_off_diagonals(
model::MOI.ModelLike,
::Type{T},
ij::Tuple{Int,Int},
f_ij::F,
f_ji::F,
) where {T,F}
diff = MOI.Utilities.operate!(-, T, f_ij, f_ji)
MOI.Utilities.canonicalize!(diff)
# The value 1e-10 was decided in https://github.com/jump-dev/JuMP.jl/pull/976
# This avoid generating symmetrization constraints when the
# functions at entries (i, j) and (j, i) are almost identical
if MOI.Utilities.isapprox_zero(diff, 1e-10)
return nothing
end
if MOI.Utilities.isapprox_zero(diff, 1e-8)
i, j = ij
@warn(
"The entries ($i, $j) and ($j, $i) of the matrix are " *
"almost identical, but a constraint has been added " *
"to ensure their equality because the largest " *
"difference between the coefficients is smaller than " *
"1e-8 but larger than 1e-10. This usually means that " *
"there is a modeling error in your formulation.",
)
end
return MOI.Utilities.normalize_and_add_constraint(
model,
diff,
MOI.EqualTo(zero(T));
allow_modify_function = true,
)
end

function bridge_constraint(
::Type{SquareBridge{T,F,G,TT,ST}},
model::MOI.ModelLike,
Expand All @@ -93,32 +145,14 @@ function bridge_constraint(
for i in 1:j
k += 1
push!(upper_triangle_indices, k)
# We constrain the entries (i, j) and (j, i) to be equal
f_ij = f_scalars[offset+i+(j-1)*dim]
f_ji = f_scalars[offset+j+(i-1)*dim]
diff = MOI.Utilities.operate!(-, T, f_ij, f_ji)
MOI.Utilities.canonicalize!(diff)
# The value 1e-10 was decided in https://github.com/jump-dev/JuMP.jl/pull/976
# This avoid generating symmetrization constraints when the
# functions at entries (i, j) and (j, i) are almost identical
if !MOI.Utilities.isapprox_zero(diff, 1e-10)
if MOI.Utilities.isapprox_zero(diff, 1e-8)
@warn(
"The entries ($i, $j) and ($j, $i) of the matrix are " *
"almost identical, but a constraint has been added " *
"to ensure their equality because the largest " *
"difference between the coefficients is smaller than " *
"1e-8 but larger than 1e-10. This usually means that " *
"there is a modeling error in your formulation.",
)
if i !== j
# We constrain the entries (i, j) and (j, i) to be equal
f_ij = f_scalars[offset+i+(j-1)*dim]
f_ji = f_scalars[offset+j+(i-1)*dim]
ci = _constrain_off_diagonals(model, T, (i, j), f_ij, f_ji)
if ci !== nothing
push!(sym, (i, j) => ci)
end
ci = MOI.Utilities.normalize_and_add_constraint(
model,
diff,
MOI.EqualTo(zero(T));
allow_modify_function = true,
)
push!(sym, (i, j) => ci)
end
end
k += dim - j
Expand Down
8 changes: 0 additions & 8 deletions src/Bridges/Constraint/bridges/vectorize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ function MOI.supports_constraint(
return true
end

function MOI.supports_constraint(
::Type{VectorizeBridge{T}},
::Type{MOI.ScalarNonlinearFunction},
::Type{<:MOI.Utilities.ScalarLinearSet{T}},
) where {T}
return false
end

function MOI.Bridges.added_constrained_variable_types(::Type{<:VectorizeBridge})
return Tuple{Type}[]
end
Expand Down
19 changes: 18 additions & 1 deletion src/Test/test_basic_constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ function _function(
)
end

function _function(
::Type{T},
::Type{MOI.VectorNonlinearFunction},
x::Vector{MOI.VariableIndex},
) where {T}
f = MOI.ScalarNonlinearFunction(
:+,
Any[MOI.ScalarNonlinearFunction(:^, Any[xi, 2]) for xi in x],
)
return MOI.VectorNonlinearFunction([f; x])
end

# Default fallback.
_set(::Any, ::Type{S}) where {S} = _set(S)

Expand Down Expand Up @@ -334,7 +346,12 @@ for s in [
:ScalarNonlinearFunction,
)
else
(:VectorOfVariables, :VectorAffineFunction, :VectorQuadraticFunction)
(
:VectorOfVariables,
:VectorAffineFunction,
:VectorQuadraticFunction,
:VectorNonlinearFunction,
)
end
for f in functions
func = Symbol("test_basic_$(f)_$(s)")
Expand Down
79 changes: 79 additions & 0 deletions src/Test/test_multiobjective.jl
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,82 @@ function test_multiobjective_vector_quadratic_function_delete_vector(
@test MOI.get(model, MOI.ObjectiveFunction{F}()) new_f
return
end

function test_multiobjective_vector_nonlinear(
model::MOI.ModelLike,
::Config{T},
) where {T}
F = MOI.VectorNonlinearFunction
@requires MOI.supports(model, MOI.ObjectiveFunction{F}())
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
f = MOI.VectorNonlinearFunction(
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
) # [x[1]^2, x[2]]
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(model, MOI.ObjectiveFunction{F}(), f)
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
@test MOI.get(model, MOI.ObjectiveFunction{F}()) f
return
end

function test_multiobjective_vector_nonlinear_delete(
model::MOI.ModelLike,
::Config{T},
) where {T}
F = MOI.VectorNonlinearFunction
@requires MOI.supports(model, MOI.ObjectiveFunction{F}())
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
f = MOI.VectorNonlinearFunction(
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
) # [x[1]^2, x[2]]
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(model, MOI.ObjectiveFunction{F}(), f)
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
@test MOI.get(model, MOI.ObjectiveFunction{F}()) f
@test_throws MOI.DeleteNotAllowed MOI.delete(model, x[1])
return
end

function test_multiobjective_vector_nonlinear_delete_vector(
model::MOI.ModelLike,
::Config{T},
) where {T}
F = MOI.VectorNonlinearFunction
@requires MOI.supports(model, MOI.ObjectiveFunction{F}())
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
f = MOI.VectorNonlinearFunction(
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
) # [x[1]^2, x[2]]
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(model, MOI.ObjectiveFunction{F}(), f)
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
@test MOI.get(model, MOI.ObjectiveFunction{F}()) f
@test_throws MOI.DeleteNotAllowed MOI.delete(model, x)
return
end

function test_multiobjective_vector_nonlinear_modify(
model::MOI.ModelLike,
::Config{T},
) where {T}
F = MOI.VectorNonlinearFunction
attr = MOI.ObjectiveFunction{F}()
@requires MOI.supports(model, attr)
x = MOI.add_variables(model, 2)
MOI.add_constraint.(model, x, MOI.GreaterThan(T(0)))
f = MOI.VectorNonlinearFunction(
Any[MOI.ScalarNonlinearFunction(:^, Any[x[1], 2]), x[2]],
) # [x[1]^2, x[2]]
MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE)
MOI.set(model, attr, f)
@test MOI.get(model, MOI.ObjectiveFunctionType()) == F
@test MOI.get(model, attr) f
@test_throws(
MOI.ModifyObjectiveNotAllowed,
MOI.modify(model, attr, MOI.VectorConstantChange(T[1, 2])),
)
return
end
90 changes: 90 additions & 0 deletions src/Test/test_nonlinear.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1673,3 +1673,93 @@ function setup_test(
model.eval_objective_value = obj_flag
end
end

function test_nonlinear_vector_complements(
model::MOI.ModelLike,
config::MOI.Test.Config{T},
) where {T}
@requires T == Float64
@requires _supports(config, MOI.optimize!)
F = MOI.ScalarNonlinearFunction
@requires MOI.supports_constraint(model, F, MOI.Complements)
x = MOI.add_variables(model, 4)
MOI.add_constraint.(model, x, MOI.Interval(T(0), T(10)))
MOI.set.(model, MOI.VariablePrimalStart(), x, T(1))
# f = [
# -1 * x3^2 + -1 * x4 + 2.0
# x3^3 + -1.0 * 2x4^2 + 2.0
# x1^5 + -1.0 * x2 + 2.0 * x3 + -2.0 * x4 + -2.0
# x1 + 2.0 * x2^3 + -2.0 * x3 + 4.0 * x4 + -6.0
# x...
# ]
f = MOI.VectorNonlinearFunction([
MOI.ScalarNonlinearFunction(
:+,
Any[
MOI.ScalarNonlinearFunction(:*, Any[-T(1), x[3], x[3]]),
MOI.ScalarNonlinearFunction(:*, Any[-T(1), x[4]]),
T(2),
],
),
MOI.ScalarNonlinearFunction(
:+,
Any[
MOI.ScalarNonlinearFunction(:^, Any[x[3], 3]),
MOI.ScalarNonlinearFunction(:*, Any[-T(2), x[4], x[4]]),
T(2),
],
),
MOI.ScalarNonlinearFunction(
:+,
Any[
MOI.ScalarNonlinearFunction(:^, Any[x[1], 5]),
MOI.ScalarAffineFunction{T}(
MOI.ScalarAffineTerm.(T[-1, 2, -2], x[2:4]),
-T(2),
),
],
),
MOI.ScalarNonlinearFunction(
:+,
Any[
MOI.ScalarNonlinearFunction(:*, Any[T(2), x[2], x[2], x[2]]),
MOI.ScalarAffineFunction{T}(
MOI.ScalarAffineTerm.(T[1, -2, 4], [x[1], x[3], x[4]]),
-T(6),
),
],
),
x[1],
x[2],
x[3],
x[4],
])
MOI.add_constraint(model, f, MOI.Complements(8))
MOI.optimize!(model)
@test MOI.get(model, MOI.TerminationStatus()) == config.optimal_status
@test (
MOI.get.(model, MOI.VariablePrimal(), x),
T[1.2847523, 0.9729165, 0.9093762, 1.1730350],
config,
)
return
end

function setup_test(
::typeof(test_nonlinear_vector_complements),
model::MOIU.MockOptimizer,
config::Config{T},
) where {T}
if T != Float64
return # Skip for non-Float64 solvers
end
MOI.Utilities.set_mock_optimize!(
model,
mock -> MOI.Utilities.mock_optimize!(
mock,
config.optimal_status,
T[1.2847523, 0.9729165, 0.9093762, 1.1730350],
),
)
return
end
Loading

0 comments on commit 92f73ae

Please sign in to comment.