diff --git a/docs/src/apimanual.md b/docs/src/apimanual.md index 66a854ff54..0bf04755d0 100644 --- a/docs/src/apimanual.md +++ b/docs/src/apimanual.md @@ -929,7 +929,9 @@ If ``\mathcal{C}_i`` is a vector set, the discussion remains valid with ``y_i(\frac{1}{2}x^TQ_ix + a_i^T x + b_i)`` replaced with the scalar product between `y_i` and the vector of scalar-valued quadratic functions. -### Constraint bridges +### Automatic reformulation + +#### Constraint reformulation A constraint often possess different equivalent formulations, but a solver may only support one of them. It would be duplicate work to implement rewritting rules in every solver wrapper for every different formulation of the constraint to express it in the form supported by the solver. @@ -966,6 +968,28 @@ model = MyPackage.Optimizer() MOI.set(model, MyPackage.PrintLevel(), 0) ``` +### Supported constrained variables and constraints + +The solver interface should only implement support for constrained variables +(see [`add_constrained_variable`](@ref)/[`add_constrained_variables`](@ref)) +or constraints that directly map to a structure exploited by the solver +algorithm. There is no need to add support for additional types, this is +handled by the [Automatic reformulation](@ref). Furthermore, this allows +[`supports_constraint`](@ref) to indicate which types are exploited by the +solver and hence allows layers such as [`Bridges.LazyBridgeOptimizer`](@ref) +to accurately select the most appropriate transformations. + +As [`add_constrained_variable`](@ref) (resp. [`add_constrained_variables`](@ref)) +falls back to [`add_variable`](@ref) (resp. [`add_variables`](@ref)) followed by +[`add_constraint`](@ref), there is no need to implement this function +if `model` supports creating free variables. However, if `model` does not +support creating free variables, then it should only implement +[`add_constrained_variable`](@ref) and not [`add_variable`](@ref) nor +[`add_constraint`](@ref) for [`SingleVariable`](@ref)-in-`typeof(set)`. +In addition, it should implement `supports_constraint(::Optimizer, +::Type{VectorOfVariables}, ::Type{Reals})` and return `false` so that free +variables are bridged, see [`supports_constraint`](@ref). + ### Implementing copy Avoid storing extra copies of the problem when possible. This means that solver diff --git a/docs/src/apireference.md b/docs/src/apireference.md index 9fa351a6a5..4b0968fcc6 100644 --- a/docs/src/apireference.md +++ b/docs/src/apireference.md @@ -182,11 +182,29 @@ delete(::ModelLike, ::Index) ### Variables -Functions for adding variables. For deleting, see index types section. +*Free variables* are the variables created with [`add_variable`](@ref) or +[`add_variables`](@ref) while *constrained variables* are +the variables created with [`add_constrained_variable`](@ref) or +[`add_constrained_variables`](@ref). Adding constrained variables instead of +constraining free variables with [`add_constraint`](@ref) allows Variable +bridges to be used. +Note further that free variables that are constrained with +[`add_constraint`](@ref) may be copied by [`copy_to`](@ref) with +[`add_constrained_variable`](@ref) or [`add_constrained_variables`](@ref) by the +[`Utilities.CachingOptimizer`](@ref). +More precisely, the attributes do not distinguish constraints on variables +created with `add_constrained_variable(s)` or `add_variable(s)`/`add_constraint`. +When the model is copied, if a variable is constrained in several sets, +the implementation of [`copy_to`](@ref) can determine whether it is added +using [`add_variable`](@ref) or [`add_constrained_variable`](@ref) with one +of the sets. The rest of the constraints on the variables are added +with [`add_constraint`](@ref). For deleting, see [Index types](@ref). ```@docs -add_variables add_variable +add_variables +add_constrained_variable +add_constrained_variables ``` List of attributes associated with variables. [category AbstractVariableAttribute] diff --git a/src/Test/UnitTests/basic_constraint_tests.jl b/src/Test/UnitTests/basic_constraint_tests.jl index 0f099c2331..3456e22e7f 100644 --- a/src/Test/UnitTests/basic_constraint_tests.jl +++ b/src/Test/UnitTests/basic_constraint_tests.jl @@ -30,7 +30,6 @@ const BasicConstraintTests = Dict( (MOI.VectorOfVariables, MOI.SOS1{Float64}) => ( dummy_vectorofvariables, 2, MOI.SOS1([1.0, 2.0]) ), (MOI.VectorOfVariables, MOI.SOS2{Float64}) => ( dummy_vectorofvariables, 2, MOI.SOS2([1.0, 2.0]) ), - (MOI.VectorOfVariables, MOI.Reals) => ( dummy_vectorofvariables, 2, MOI.Reals(2) ), (MOI.VectorOfVariables, MOI.Zeros) => ( dummy_vectorofvariables, 2, MOI.Zeros(2) ), (MOI.VectorOfVariables, MOI.Nonpositives) => ( dummy_vectorofvariables, 2, MOI.Nonpositives(2) ), (MOI.VectorOfVariables, MOI.Nonnegatives) => ( dummy_vectorofvariables, 2, MOI.Nonnegatives(2) ), @@ -60,7 +59,6 @@ const BasicConstraintTests = Dict( (MOI.ScalarQuadraticFunction{Float64}, MOI.EqualTo{Float64}) => ( dummy_scalar_quadratic, 1, MOI.EqualTo(1.0) ), (MOI.ScalarQuadraticFunction{Float64}, MOI.Interval{Float64}) => ( dummy_scalar_quadratic, 1, MOI.Interval(1.0, 2.0) ), - (MOI.VectorAffineFunction{Float64}, MOI.Reals) => ( dummy_vector_affine, 2, MOI.Reals(2) ), (MOI.VectorAffineFunction{Float64}, MOI.Zeros) => ( dummy_vector_affine, 2, MOI.Zeros(2) ), (MOI.VectorAffineFunction{Float64}, MOI.Nonpositives) => ( dummy_vector_affine, 2, MOI.Nonpositives(2) ), (MOI.VectorAffineFunction{Float64}, MOI.Nonnegatives) => ( dummy_vector_affine, 2, MOI.Nonnegatives(2) ), @@ -68,7 +66,6 @@ const BasicConstraintTests = Dict( (MOI.VectorAffineFunction{Float64}, MOI.SecondOrderCone) => ( dummy_vector_affine, 3, MOI.SecondOrderCone(3) ), (MOI.VectorAffineFunction{Float64}, MOI.RotatedSecondOrderCone) => ( dummy_vector_affine, 3, MOI.RotatedSecondOrderCone(3) ), - (MOI.VectorQuadraticFunction{Float64}, MOI.Reals) => ( dummy_vector_quadratic, 2, MOI.Reals(2) ), (MOI.VectorQuadraticFunction{Float64}, MOI.Zeros) => ( dummy_vector_quadratic, 2, MOI.Zeros(2) ), (MOI.VectorQuadraticFunction{Float64}, MOI.Nonpositives) => ( dummy_vector_quadratic, 2, MOI.Nonpositives(2) ), (MOI.VectorQuadraticFunction{Float64}, MOI.Nonnegatives) => ( dummy_vector_quadratic, 2, MOI.Nonnegatives(2) ), diff --git a/src/constraints.jl b/src/constraints.jl index f288827410..36ca0fbe22 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -8,10 +8,28 @@ is, `copy_to(model, src)` does not throw [`UnsupportedConstraint`](@ref) when `src` contains `F`-in-`S` constraints. If `F`-in-`S` constraints are only not supported in specific circumstances, e.g. `F`-in-`S` constraints cannot be combined with another type of constraint, it should still return `true`. -""" + + supports_constraint(model::ModelLike, ::Type{VectorOfVariables}, ::Type{Reals})::Bool + +Return a `Bool` indicating whether `model` supports free variables. That is, +`copy_to(model, src)` does not error when `src` contains variables that are not +constrained by any [`SingleVariable`](@ref) or [`VectorOfVariables`](@ref) +constraint. By default, this method returns `true` so it should only be +implemented if `model` does not support free variables. For instance, if a +solver requires all variables to be nonnegative, it should implement this +method and return `false` because free variables cannot be copied to the solver. + +Note that free variables are not explicitly set to be free by calling +[`add_constraint`](@ref) with the set [`Reals`](@ref), instead, free variables +are created with [`add_variable`](@ref) and [`add_variables`](@ref). +If `model` does not support free variables, it should not implement +[`add_variable`](@ref) nor [`add_variables`](@ref) but should implement +this method and return `false`. This allows free variables to be bridged as the +sum of a nonnegative and a nonpositive variables. +""" # Implemented as only one method to avoid ambiguity function supports_constraint(model::ModelLike, F::Type{<:AbstractFunction}, S::Type{<:AbstractSet}) - return false + return F == VectorOfVariables && S == Reals end """ diff --git a/src/variables.jl b/src/variables.jl index 0b8438596d..f37f9e49a4 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -33,3 +33,70 @@ A [`AddVariableNotAllowed`](@ref) error is thrown if adding variables cannot be done in the current state of the model `model`. """ add_variable(model::ModelLike) = throw(AddVariableNotAllowed()) + +""" + add_constrained_variable( + model::ModelLike, + set::AbstractScalarSet + )::Tuple{MOI.VariableIndex, + MOI.ConstraintIndex{MOI.SingleVariable, typeof(set)}} + +Add to `model` a scalar variable constrained to belong to `set`, returning the +index of the variable created and the index of the constraint constraining the +variable to belong to `set`. + +By default, this function falls back to creating a free variable with +[`add_variable`](@ref) and then constraining it to belong to `set` with +[`add_constraint`](@ref). +""" +function add_constrained_variable(model::ModelLike, set::AbstractScalarSet) + variable = add_variable(model) + constraint = add_constraint(model, SingleVariable(variable), set) + return variable, constraint +end + +""" + add_constrained_variables( + model::ModelLike, + sets:AbstractVector{<:AbstractScalarSet} + )::Tuple{Vector{MOI.VariableIndex}, + Vector{MOI.ConstraintIndex{MOI.SingleVariable, eltype(sets)}}} + +Add to `model` scalar variables constrained to belong to `sets`, returning the +indices of the variables created and the indices of the constraints constraining +the variables to belong to each set in `sets`. That is, if it returns `variables` +and `constraints`, `constraints[i]` is the index of the constraint constraining +`variable[i]` to belong to `sets[i]`. + +By default, this function falls back to calling +[`add_constrained_variable`](@ref) on each set. +""" +function add_constrained_variables(model::ModelLike, sets::AbstractVector{<:AbstractScalarSet}) + variables = Vector{VariableIndex}(undef, length(sets)) + constraints = Vector{ConstraintIndex{SingleVariable, eltype(sets)}}(undef, length(sets)) + for (i, set) in enumerate(sets) + variables[i], constraints[i] = add_constrained_variable(model, set) + end + return variables, constraints +end + +""" + add_constrained_variables( + model::ModelLike, + set::AbstractVectorSet + )::Tuple{Vector{MOI.VariableIndex}, + MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}} + +Add to `model` a vector of variables constrained to belong to `set`, returning +the indices of the variables created and the index of the constraint +constraining the vector of variables to belong to `set`. + +By default, this function falls back to creating free variables with +[`add_variables`](@ref) and then constraining it to belong to `set` with +[`add_constraint`](@ref). +""" +function add_constrained_variables(model::ModelLike, set::AbstractVectorSet) + variables = add_variables(model, dimension(set)) + constraint = add_constraint(model, VectorOfVariables(variables), set) + return variables, constraint +end