Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VectorNonlinearFunction #2201

Merged
merged 29 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
odow marked this conversation as resolved.
Show resolved Hide resolved
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]
odow marked this conversation as resolved.
Show resolved Hide resolved
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]
odow marked this conversation as resolved.
Show resolved Hide resolved
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]
odow marked this conversation as resolved.
Show resolved Hide resolved
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
Loading