diff --git a/docs/src/reference/standard_form.md b/docs/src/reference/standard_form.md index c291483603..ffa3efac0c 100644 --- a/docs/src/reference/standard_form.md +++ b/docs/src/reference/standard_form.md @@ -67,6 +67,7 @@ Integer ZeroOne Semicontinuous Semiinteger +Parameter ``` ## Vector sets diff --git a/src/Bridges/Variable/Variable.jl b/src/Bridges/Variable/Variable.jl index d3350e1149..5c905c9146 100644 --- a/src/Bridges/Variable/Variable.jl +++ b/src/Bridges/Variable/Variable.jl @@ -21,6 +21,7 @@ include("bridges/soc_rsoc.jl") include("bridges/vectorize.jl") include("bridges/zeros.jl") include("bridges/hermitian.jl") +include("bridges/parameter.jl") """ add_all_bridges(model, ::Type{T}) where {T} @@ -38,6 +39,7 @@ function add_all_bridges(model, ::Type{T}) where {T} MOI.Bridges.add_bridge(model, RSOCtoSOCBridge{T}) MOI.Bridges.add_bridge(model, RSOCtoPSDBridge{T}) MOI.Bridges.add_bridge(model, HermitianToSymmetricPSDBridge{T}) + MOI.Bridges.add_bridge(model, ParameterToEqualToBridge{T}) return end diff --git a/src/Bridges/Variable/bridges/parameter.jl b/src/Bridges/Variable/bridges/parameter.jl new file mode 100644 index 0000000000..794d6a2c98 --- /dev/null +++ b/src/Bridges/Variable/bridges/parameter.jl @@ -0,0 +1,117 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +""" + ParameterToEqualToBridge{T} <: Bridges.Variable.AbstractBridge + +`ParameterToEqualToBridge` implements the following reformulation: + +* ``x \\in Parameter(v)`` into ``x == v`` + +## Source node + +`ParameterToEqualToBridge` supports: + + * [`MOI.VariableIndex`](@ref) in [`MOI.Parameter`](@ref) + +## Target nodes + +`ParameterToEqualToBridge` creates: + + * One variable node: [`MOI.VariableIndex`](@ref) in [`MOI.EqualTo{T}`](@ref) +""" +struct ParameterToEqualToBridge{T} <: AbstractBridge + x::MOI.VariableIndex + ci::MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}} +end + +const ParameterToEqualTo{T,OT<:MOI.ModelLike} = + SingleBridgeOptimizer{ParameterToEqualToBridge{T},OT} + +function bridge_constrained_variable( + ::Type{ParameterToEqualToBridge{T}}, + model::MOI.ModelLike, + set::MOI.Parameter{T}, +) where {T} + x, ci = MOI.add_constrained_variable(model, MOI.EqualTo(set.value)) + return ParameterToEqualToBridge{T}(x, ci) +end + +function supports_constrained_variable( + ::Type{ParameterToEqualToBridge{T}}, + ::Type{MOI.Parameter{T}}, +) where {T} + return true +end + +function MOI.Bridges.added_constrained_variable_types( + ::Type{ParameterToEqualToBridge{T}}, +) where {T} + return Tuple{Type}[(MOI.EqualTo{T},)] +end + +function MOI.Bridges.added_constraint_types(::Type{<:ParameterToEqualToBridge}) + return Tuple{Type,Type}[] +end + +MOI.get(bridge::ParameterToEqualToBridge, ::MOI.NumberOfVariables)::Int64 = 1 + +function MOI.get(bridge::ParameterToEqualToBridge, ::MOI.ListOfVariableIndices) + return [bridge.x] +end + +function MOI.get( + ::ParameterToEqualToBridge{T}, + ::MOI.NumberOfConstraints{MOI.VariableIndex,MOI.EqualTo{T}}, +)::Int64 where {T} + return 1 +end + +function MOI.get( + bridge::ParameterToEqualToBridge{T}, + ::MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.EqualTo{T}}, +) where {T} + return [bridge.ci] +end + +function MOI.delete(model::MOI.ModelLike, bridge::ParameterToEqualToBridge) + MOI.delete(model, bridge.x) + return +end + +function MOI.get( + model::MOI.ModelLike, + ::MOI.ConstraintSet, + bridge::ParameterToEqualToBridge{T}, +) where {T} + set = MOI.get(model, MOI.ConstraintSet(), bridge.ci) + return MOI.Parameter(set.value) +end + +function MOI.get( + model::MOI.ModelLike, + attr::Union{MOI.ConstraintFunction,MOI.ConstraintPrimal,MOI.ConstraintDual}, + bridge::ParameterToEqualToBridge, +) + return MOI.get(model, attr, bridge.ci) +end + +function _to_one(::Type{T}, x) where {T} + return MOI.ScalarAffineFunction([MOI.ScalarAffineTerm(one(T), x)], zero(T)) +end + +function MOI.Bridges.bridged_function( + bridge::ParameterToEqualToBridge{T}, +) where {T} + return _to_one(T, bridge.x) +end + +function unbridged_map( + bridge::ParameterToEqualToBridge{T}, + x::MOI.VariableIndex, +) where {T} + return [bridge.x => _to_one(T, x)] +end diff --git a/src/Test/test_basic_constraint.jl b/src/Test/test_basic_constraint.jl index 9b15a67f8f..5387698e22 100644 --- a/src/Test/test_basic_constraint.jl +++ b/src/Test/test_basic_constraint.jl @@ -72,6 +72,7 @@ _set(::Any, ::Type{S}) where {S} = _set(S) _set(::Type{T}, ::Type{MOI.LessThan}) where {T} = MOI.LessThan(one(T)) _set(::Type{T}, ::Type{MOI.GreaterThan}) where {T} = MOI.GreaterThan(one(T)) _set(::Type{T}, ::Type{MOI.EqualTo}) where {T} = MOI.EqualTo(one(T)) +_set(::Type{T}, ::Type{MOI.Parameter}) where {T} = MOI.Parameter(one(T)) _set(::Type{T}, ::Type{MOI.Interval}) where {T} = MOI.Interval(zero(T), one(T)) _set(::Type{MOI.ZeroOne}) = MOI.ZeroOne() _set(::Type{MOI.Integer}) = MOI.Integer() @@ -264,6 +265,7 @@ for s in [ :GreaterThan, :LessThan, :EqualTo, + :Parameter, :Interval, :ZeroOne, :Semicontinuous, diff --git a/src/Test/test_variable.jl b/src/Test/test_variable.jl index 4b65ac2a82..e6dd460044 100644 --- a/src/Test/test_variable.jl +++ b/src/Test/test_variable.jl @@ -571,3 +571,29 @@ function test_add_constrained_variables_vector( @test MOI.get(model, MOI.ConstraintSet(), cv[2]) == sets[2] return end + +""" + test_add_parameter(model::MOI.ModelLike, config::Config) + +Test adding a variable in [`MOI.Parameter`](@ref). +""" +function test_add_parameter(model::MOI.ModelLike, ::Config{T}) where {T} + @requires MOI.supports_add_constrained_variable(model, MOI.Parameter{T}) + @test MOI.get(model, MOI.NumberOfVariables()) == 0 + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(one(T))) + @test MOI.get(model, MOI.NumberOfVariables()) == 1 + @test MOI.get(model, MOI.ConstraintSet(), ci) == MOI.Parameter(one(T)) + @test MOI.get(model, MOI.ConstraintFunction(), ci) == x + F, S = MOI.VariableIndex, MOI.Parameter{T} + @test MOI.get(model, MOI.NumberOfConstraints{F,S}()) == 1 + @test MOI.get(model, MOI.ListOfConstraintIndices{F,S}()) == [ci] + @test_throws( + MOI.LowerBoundAlreadySet, + MOI.add_constraint(model, x, MOI.GreaterThan(one(T))), + ) + @test_throws( + MOI.UpperBoundAlreadySet, + MOI.add_constraint(model, x, MOI.LessThan(one(T))), + ) + return +end diff --git a/src/Utilities/model.jl b/src/Utilities/model.jl index e437b02174..1c8c65ddb6 100644 --- a/src/Utilities/model.jl +++ b/src/Utilities/model.jl @@ -770,6 +770,7 @@ const LessThanIndicatorZero{T} = MOI.Interval, MOI.Semicontinuous, MOI.Semiinteger, + MOI.Parameter, ), ( MOI.Reals, diff --git a/src/Utilities/variables_container.jl b/src/Utilities/variables_container.jl index fdc0bd718a..2e4c63d28b 100644 --- a/src/Utilities/variables_container.jl +++ b/src/Utilities/variables_container.jl @@ -47,6 +47,14 @@ function set_from_constants( return S() end +function set_from_constants( + b::AbstractVectorBounds, + ::Type{<:MOI.Parameter}, + index, +) + return MOI.Parameter(b.lower[index]) +end + """ SUPPORTED_VARIABLE_SCALAR_SETS{T} @@ -62,12 +70,13 @@ const SUPPORTED_VARIABLE_SCALAR_SETS{T} = Union{ MOI.ZeroOne, MOI.Semicontinuous{T}, MOI.Semiinteger{T}, + MOI.Parameter{T}, } -# 0xcb = 0x0080 | 0x0040 | 0x0008 | 0x0002 | 0x0001 -const _LOWER_BOUND_MASK = 0x00cb -# 0xcd = 0x0080 | 0x0040 | 0x0008 | 0x0004 | 0x0001 -const _UPPER_BOUND_MASK = 0x00cd +# 0x01cb = 0x0080 | 0x0040 | 0x0008 | 0x0002 | 0x0001 | 0x0100 +const _LOWER_BOUND_MASK = 0x01cb +# 0x01cd = 0x0080 | 0x0040 | 0x0008 | 0x0004 | 0x0001 | 0x0100 +const _UPPER_BOUND_MASK = 0x01cd const _DELETED_VARIABLE = 0x8000 @@ -79,6 +88,7 @@ _single_variable_flag(::Type{MOI.Integer}) = 0x0010 _single_variable_flag(::Type{MOI.ZeroOne}) = 0x0020 _single_variable_flag(::Type{<:MOI.Semicontinuous}) = 0x0040 _single_variable_flag(::Type{<:MOI.Semiinteger}) = 0x0080 +_single_variable_flag(::Type{<:MOI.Parameter}) = 0x0100 function _flag_to_set_type(flag::UInt16, ::Type{T}) where {T} if flag == 0x0001 @@ -95,9 +105,11 @@ function _flag_to_set_type(flag::UInt16, ::Type{T}) where {T} return MOI.ZeroOne elseif flag == 0x0040 return MOI.Semicontinuous{T} - else - @assert flag == 0x0080 + elseif flag == 0x0080 return MOI.Semiinteger{T} + else + @assert flag == 0x0100 + return MOI.Parameter{T} end end @@ -143,7 +155,7 @@ function _lower_bound( return set.lower end -_lower_bound(set::MOI.EqualTo) = set.value +_lower_bound(set::Union{MOI.EqualTo,MOI.Parameter}) = set.value function _upper_bound( set::Union{MOI.LessThan,MOI.Interval,MOI.Semicontinuous,MOI.Semiinteger}, @@ -151,7 +163,7 @@ function _upper_bound( return set.upper end -_upper_bound(set::MOI.EqualTo) = set.value +_upper_bound(set::Union{MOI.EqualTo,MOI.Parameter}) = set.value # Use `-Inf` and `Inf` for `AbstractFloat` subtypes. _no_lower_bound(::Type{T}) where {T} = zero(T) @@ -373,6 +385,7 @@ function MOI.get( _add_constraint_type(list, b, MOI.Semiinteger{T}) _add_constraint_type(list, b, MOI.Integer) _add_constraint_type(list, b, MOI.ZeroOne) + _add_constraint_type(list, b, MOI.Parameter{T}) return list end diff --git a/src/sets.jl b/src/sets.jl index 6c50fe7979..5bc010b3f2 100644 --- a/src/sets.jl +++ b/src/sets.jl @@ -236,10 +236,47 @@ struct EqualTo{T<:Number} <: AbstractScalarSet value::T end +""" + Parameter{T<:Number}(value::T) + +The set containing the single point ``x \\in \\mathbb{R}`` where ``x`` is given +by `value`. + +The `Parameter` set is conceptually similar to the [`EqualTo`](@ref) set, except +that a variable constrained to the `Parameter` set cannot have other constraints +added to it, and the `Parameter` set can never be deleted. Thus, solvers are +free to treat the variable as a constant, and they need not add it as a decision +variable to the model. + +Because of this behavior, you must add parameters using [`add_constrained_variable`](@ref), +and solvers should declare [`supports_add_constrained_variable`](@ref) and not +[`supports_constraint`](@ref) for the `Parameter` set. + +## Example + +```jldoctest +julia> import MathOptInterface as MOI + +julia> model = MOI.Utilities.Model{Float64}() +MOIU.Model{Float64} + +julia> p, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.5)) +(MathOptInterface.VariableIndex(1), MathOptInterface.ConstraintIndex{MathOptInterface.VariableIndex, MathOptInterface.Parameter{Float64}}(1)) + +julia> MOI.set(model, MOI.ConstraintSet(), ci, MOI.Parameter(3.0)) + +julia> MOI.get(model, MOI.ConstraintSet(), ci) +MathOptInterface.Parameter{Float64}(3.0) +``` +""" +struct Parameter{T<:Number} <: AbstractScalarSet + value::T +end + function Base.:(==)( set1::S, set2::S, -) where {S<:Union{GreaterThan,LessThan,EqualTo}} +) where {S<:Union{GreaterThan,LessThan,EqualTo,Parameter}} return constant(set1) == constant(set2) end @@ -278,6 +315,7 @@ Returns the constant of the set. constant(s::EqualTo) = s.value constant(s::GreaterThan) = s.lower constant(s::LessThan) = s.upper +constant(s::Parameter) = s.value """ NormInfinityCone(dimension) @@ -1875,6 +1913,7 @@ function Base.copy( GreaterThan, LessThan, EqualTo, + Parameter, Interval, NormInfinityCone, NormOneCone, diff --git a/test/Bridges/Variable/parameter.jl b/test/Bridges/Variable/parameter.jl new file mode 100644 index 0000000000..872ee11298 --- /dev/null +++ b/test/Bridges/Variable/parameter.jl @@ -0,0 +1,86 @@ +# Copyright (c) 2017: Miles Lubin and contributors +# Copyright (c) 2017: Google Inc. +# +# Use of this source code is governed by an MIT-style license that can be found +# in the LICENSE.md file or at https://opensource.org/licenses/MIT. + +module TestVariableParameter + +using Test + +using MathOptInterface +const MOI = MathOptInterface + +function runtests() + for name in names(@__MODULE__; all = true) + if startswith("$(name)", "test_") + @testset "$(name)" begin + getfield(@__MODULE__, name)() + end + end + end + return +end + +function test_runtests() + MOI.Bridges.runtests( + MOI.Bridges.Variable.ParameterToEqualToBridge, + """ + constrainedvariable: x in Parameter(2.0) + minobjective: 1.0 * x + 2.0 + """, + """ + constrainedvariable: x in EqualTo(2.0) + minobjective: 1.0 * x + 2.0 + """, + ) + MOI.Bridges.runtests( + MOI.Bridges.Variable.ParameterToEqualToBridge, + """ + constrainedvariable: x in Parameter(-2.0) + minobjective: 1.0 * x + 2.0 + """, + """ + constrainedvariable: x in EqualTo(-2.0) + minobjective: 1.0 * x + 2.0 + """, + ) + return +end + +function test_delete() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Variable.ParameterToEqualTo{Float64}(inner) + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + @test !MOI.is_empty(inner) + MOI.delete(model, x) + @test MOI.is_empty(inner) + @test MOI.is_empty(model) + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + @test_throws MOI.DeleteNotAllowed MOI.delete(model, ci) + return +end + +function test_constraint_function() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Variable.ParameterToEqualTo{Float64}(inner) + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + y, bridge = first(model.map) + @test y == x + x_inner = MOI.get(model, MOI.ConstraintFunction(), bridge) + @test MOI.get(inner, MOI.ListOfVariableIndices()) == [x_inner] + return +end + +function test_list_of_constraint_indices() + inner = MOI.Utilities.Model{Float64}() + model = MOI.Bridges.Variable.ParameterToEqualTo{Float64}(inner) + x, ci = MOI.add_constrained_variable(model, MOI.Parameter(2.0)) + F, S = MOI.VariableIndex, MOI.EqualTo{Float64} + @test isempty(MOI.get(model, MOI.ListOfConstraintIndices{F,S}())) + return +end + +end # module + +TestVariableParameter.runtests() diff --git a/test/Utilities/universalfallback.jl b/test/Utilities/universalfallback.jl index da1e09c3f5..aa15019a1c 100644 --- a/test/Utilities/universalfallback.jl +++ b/test/Utilities/universalfallback.jl @@ -83,6 +83,7 @@ function test_MOI_Test() # Bugs in UniversalFallback "test_model_LowerBoundAlreadySet", "test_model_UpperBoundAlreadySet", + "test_add_parameter", ], ) return