From b02cb7012c9ef7a2d08880ac8c291e190e7ce3ca Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Feb 2025 15:47:58 +1300 Subject: [PATCH 1/5] Add VariableInSetRef and has_variable_in_set --- docs/src/manual/variables.md | 34 +++++ src/JuMP.jl | 3 + src/variables.jl | 253 +++++++++++++++++++++++++++++++---- test/test_variable.jl | 81 +++++++++++ 4 files changed, 347 insertions(+), 24 deletions(-) diff --git a/docs/src/manual/variables.md b/docs/src/manual/variables.md index 574a31053b2..627a526cbcb 100644 --- a/docs/src/manual/variables.md +++ b/docs/src/manual/variables.md @@ -1308,6 +1308,40 @@ julia> x = @variable(model, [1:3], set = SecondOrderCone()) You cannot delete the constraint associated with a variable constrained on creation. +To check if a variable was constrained on creation, use [`has_variable_in_set`](@ref), +and use [`VariableInSetRef`](@ref) to obtain the associated constraint reference: + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x[1:2, 1:2], PSD) +2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}: + x[1,1] x[1,2] + x[1,2] x[2,2] + +julia> has_variable_in_set(x) +true + +julia> c = VariableInSetRef(x) +[x[1,1] x[1,2] + ⋯ x[2,2]] ∈ PSDCone() + +julia> @variable(model, y) +y + +julia> has_variable_in_set(y) +false + +julia> @variable(model, z in Semicontinuous(1, 2)) +z + +julia> has_variable_in_set(z) +true + +julia> c_z = VariableInSetRef(z) +z ∈ MathOptInterface.Semicontinuous{Int64}(1, 2) +``` + ### Example: positive semidefinite variables An alternative to the syntax in [Semidefinite variables](@ref), declare a matrix diff --git a/src/JuMP.jl b/src/JuMP.jl index f1dbfc4e797..f46ddec77c2 100644 --- a/src/JuMP.jl +++ b/src/JuMP.jl @@ -124,6 +124,8 @@ mutable struct GenericModel{T<:Real} <: AbstractModel # A model-level option that is used as the default for the set_string_name # keyword to @variable and @constraint. set_string_names_on_creation::Bool + # + variable_in_set_ref::Dict{Any,MOI.ConstraintIndex} end value_type(::Type{GenericModel{T}}) where {T} = T @@ -239,6 +241,7 @@ function direct_generic_model( false, Dict{Symbol,Any}(), true, + Dict{Any,MOI.ConstraintIndex}(), ) end diff --git a/src/variables.jl b/src/variables.jl index 85277a7ea65..cf9b4b15476 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -1752,6 +1752,188 @@ function parameter_value(x::GenericVariableRef) return set.value end +# VariableInSetRef + +""" + has_variable_in_set( + model::GenericModel, + x::Union{AbstractJuMPScalar,AbstractArray{<:AbstractJuMPScalar}}, + )::Bool + +Return a `Bool` if [`VariableInSetRef`](@ref) returns a valid constraint +reference without erroring. + +## Exceptions + +This function does not apply for variable bounds or integrality restrictions of +a scalar variable. For example: + +```jldoctest variable_in_set_ref_docstring +julia> model = Model(); + +julia> @variable(model, x >= 0, Int) +x + +julia> has_variable_in_set(x) +false +``` + +Use instead [`is_integer`](@ref), [`is_binary`](@ref), [`has_lower_bound`](@ref), +[`has_upper_bound`](@ref), and [`is_fixed`](@ref). + +```jldoctest variable_in_set_ref_docstring +julia> model = Model(); + +julia> @variable(model, x >= 0, Int) +x + +julia> is_integer(x) +true + +julia> has_lower_bound(x) +true +``` + +## Example + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x[1:2, 1:2], PSD) +2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}: + x[1,1] x[1,2] + x[1,2] x[2,2] + +julia> has_variable_in_set(x) +true + +julia> c = VariableInSetRef(x) +[x[1,1] x[1,2] + ⋯ x[2,2]] ∈ PSDCone() + +julia> @variable(model, y) +y + +julia> has_variable_in_set(y) +false + +julia> @variable(model, z in Semicontinuous(1, 2)) +z + +julia> has_variable_in_set(z) +true + +julia> c_z = VariableInSetRef(z) +z ∈ MathOptInterface.Semicontinuous{Int64}(1, 2) +``` +""" +function has_variable_in_set(x::AbstractJuMPScalar) + model = owner_model(x) + return haskey(model.variable_in_set_ref, x) +end + +function has_variable_in_set(x::AbstractArray{<:AbstractJuMPScalar}) + model = owner_model(first(x)) + return haskey(model.variable_in_set_ref, x) +end + +""" + VariableInSetRef( + model::GenericModel, + x::Union{AbstractJuMPScalar,AbstractArray{<:AbstractJuMPScalar}}, + ) + +Return the constraint reference associated with `x` when it is constrained on +creation. + +A variable is constrained on creation if it uses the `x in S` or `x, set = S` +syntax in the [`@variable`](@ref) macro. + +This function errors if `x` was not constrained on creation. To check if the +variable was constrained on creation, use [`has_variable_in_set`](@ref). + +## Exceptions + +This function does not apply for variable bounds or integrality restrictions of +a scalar variable. For example: + +```jldoctest variable_in_set_ref_docstring +julia> model = Model(); + +julia> @variable(model, x >= 0, Int) +x + +julia> has_variable_in_set(x) +false +``` + +Use instead [`IntegerRef`](@ref), [`BinaryRef`](@ref), [`LowerBoundRef`](@ref), +[`UpperBoundRef`](@ref), and [`FixRef`](@ref). + +```jldoctest variable_in_set_ref_docstring +julia> model = Model(); + +julia> @variable(model, x >= 0, Int) +x + +julia> IntegerRef(x) +x integer + +julia> LowerBoundRef(x) +x ≥ 0 +``` + +## Example + +```jldoctest +julia> model = Model(); + +julia> @variable(model, x[1:2, 1:2], PSD) +2×2 LinearAlgebra.Symmetric{VariableRef, Matrix{VariableRef}}: + x[1,1] x[1,2] + x[1,2] x[2,2] + +julia> has_variable_in_set(x) +true + +julia> c = VariableInSetRef(x) +[x[1,1] x[1,2] + ⋯ x[2,2]] ∈ PSDCone() + +julia> @variable(model, y) +y + +julia> has_variable_in_set(y) +false + +julia> @variable(model, z in Semicontinuous(1, 2)) +z + +julia> has_variable_in_set(z) +true + +julia> c_z = VariableInSetRef(z) +z ∈ MathOptInterface.Semicontinuous{Int64}(1, 2) +``` +""" +function VariableInSetRef(x::AbstractJuMPScalar) + model = owner_model(x) + ci = get(model.variable_in_set_ref, x, nothing) + if ci === nothing + error("VariableInSetRef does not exist for $x") + end + return constraint_ref_with_index(model, ci) +end + +function VariableInSetRef(x::AbstractArray{<:AbstractJuMPScalar}) + model = owner_model(first(x)) + ci = get(model.variable_in_set_ref, x, nothing) + if ci === nothing + error("VariableInSetRef does not exist for $x") + end + return constraint_ref_with_index(model, ci) +end + # MOI.VariablePrimalStart """ @@ -1968,8 +2150,7 @@ function _moi_add_constrained_variable( ::Nothing, set::MOI.AbstractScalarSet, ) - x, _ = MOI.add_constrained_variable(moi_backend, set) - return x + return MOI.add_constrained_variable(moi_backend, set) end function _moi_add_constrained_variable( @@ -1977,8 +2158,8 @@ function _moi_add_constrained_variable( x::MOI.VariableIndex, set::MOI.AbstractScalarSet, ) - MOI.add_constraint(moi_backend, x, set) - return x + ci = MOI.add_constraint(moi_backend, x, set) + return x, ci end function _moi_add_constrained_variable( @@ -1986,8 +2167,7 @@ function _moi_add_constrained_variable( ::Nothing, set::Tuple{MOI.GreaterThan{T},MOI.LessThan{T}}, ) where {T<:Real} - x, _ = MOI.add_constrained_variable(moi_backend, set) - return x + return MOI.add_constrained_variable(moi_backend, set) end function _moi_add_variable( @@ -2006,24 +2186,26 @@ function _moi_add_variable( MOI.GreaterThan{T}(_to_value(T, info.lower_bound, "lower bound")), MOI.LessThan{T}(_to_value(T, info.upper_bound, "upper bound")), ) - index = _moi_add_constrained_variable(moi_backend, index, set) + index, _ = _moi_add_constrained_variable(moi_backend, index, set) elseif info.has_lb set_lb = MOI.GreaterThan{T}(_to_value(T, info.lower_bound, "lower bound")) - index = _moi_add_constrained_variable(moi_backend, index, set_lb) + index, _ = _moi_add_constrained_variable(moi_backend, index, set_lb) elseif info.has_ub set_ub = MOI.LessThan{T}(_to_value(T, info.upper_bound, "upper bound")) - index = _moi_add_constrained_variable(moi_backend, index, set_ub) + index, _ = _moi_add_constrained_variable(moi_backend, index, set_ub) end if info.has_fix set_eq = MOI.EqualTo{T}(_to_value(T, info.fixed_value, "fixed value")) - index = _moi_add_constrained_variable(moi_backend, index, set_eq) + index, _ = _moi_add_constrained_variable(moi_backend, index, set_eq) end if info.binary - index = _moi_add_constrained_variable(moi_backend, index, MOI.ZeroOne()) + index, _ = + _moi_add_constrained_variable(moi_backend, index, MOI.ZeroOne()) end if info.integer - index = _moi_add_constrained_variable(moi_backend, index, MOI.Integer()) + index, _ = + _moi_add_constrained_variable(moi_backend, index, MOI.Integer()) end if index === nothing index = MOI.add_variable(moi_backend) @@ -2129,14 +2311,16 @@ function add_variable( variable::VariableConstrainedOnCreation, name::String, ) where {T} - var_index = _moi_add_constrained_variable( + x, ci = _moi_add_constrained_variable( backend(model), variable.scalar_variable, variable.set, name, T, ) - return GenericVariableRef(model, var_index) + ret = GenericVariableRef(model, x) + model.variable_in_set_ref[ret] = ci + return ret end function add_variable( @@ -2159,7 +2343,7 @@ function _moi_add_constrained_variable( if !isempty(name) MOI.set(moi_backend, MOI.VariableName(), var_index, name) end - return var_index + return var_index, con_index end """ @@ -2215,16 +2399,20 @@ function add_variable( variable::VariablesConstrainedOnCreation, names, ) where {T} - var_indices = _moi_add_constrained_variables( + x, ci = _moi_add_constrained_variables( backend(model), variable.scalar_variables, variable.set, vectorize(names, variable.shape), T, ) - var_refs = - [GenericVariableRef{T}(model, var_index) for var_index in var_indices] - return reshape_vector(var_refs, variable.shape) + var_refs = [GenericVariableRef{T}(model, xi) for xi in x] + ret = reshape_vector(var_refs, variable.shape) + model.variable_in_set_ref[ret] = ci + if !(variable.shape isa ScalarShape) && !(variable.shape isa VectorShape) + model.shapes[ci] = variable.shape + end + return ret end function _moi_add_constrained_variables( @@ -2234,11 +2422,28 @@ function _moi_add_constrained_variables( names::Union{Vector{String},Nothing}, ::Type{T}, ) where {T} - if set isa MOI.Reals - var_indices = MOI.add_variables(moi_backend, MOI.dimension(set)) - else - var_indices, con_index = MOI.add_constrained_variables(moi_backend, set) + var_indices, con_index = MOI.add_constrained_variables(moi_backend, set) + for (index, variable) in zip(var_indices, scalar_variables) + _moi_constrain_variable(moi_backend, index, variable.info, T) end + if names !== nothing + for (var_index, name) in zip(var_indices, names) + if !isempty(name) + MOI.set(moi_backend, MOI.VariableName(), var_index, name) + end + end + end + return var_indices, con_index +end + +function _moi_add_constrained_variables( + moi_backend::MOI.ModelLike, + scalar_variables::Vector{<:ScalarVariable}, + set::MOI.Reals, + names::Union{Vector{String},Nothing}, + ::Type{T}, +) where {T} + var_indices = MOI.add_variables(moi_backend, MOI.dimension(set)) for (index, variable) in zip(var_indices, scalar_variables) _moi_constrain_variable(moi_backend, index, variable.info, T) end @@ -2249,7 +2454,7 @@ function _moi_add_constrained_variables( end end end - return var_indices + return var_indices, nothing end """ diff --git a/test/test_variable.jl b/test/test_variable.jl index d61675dd484..3aa3bfb5caf 100644 --- a/test/test_variable.jl +++ b/test/test_variable.jl @@ -1700,4 +1700,85 @@ function test_add_variable_in_set() return end +function test_variable_in_set_scalar() + model = Model() + @variable(model, x, set = MOI.Integer()) + c = VariableInSetRef(x) + @test has_variable_in_set(x) + @variable(model, y, Int) + @test !has_variable_in_set(y) + @test_throws( + ErrorException("VariableInSetRef does not exist for $y"), + VariableInSetRef(y), + ) + for set in ( + MOI.GreaterThan(1.0), + MOI.LessThan(1.0), + MOI.EqualTo(1.0), + MOI.Interval(1.0, 2.0), + MOI.Integer(), + MOI.ZeroOne(), + MOI.Semicontinuous(1.0, 2.0), + MOI.Semiinteger(1.0, 2.0), + ) + model = Model() + @variable(model, x in set) + @test has_variable_in_set(x) + c = VariableInSetRef(x) + o = constraint_object(c) + @test isequal_canonical(o.func, x) + @test o.set == set + end + return +end + +function test_variable_in_set_vector() + model = Model() + @variable(model, x, set = MOI.Integer()) + c = VariableInSetRef(x) + @test has_variable_in_set(x) + @variable(model, y, Int) + @test !has_variable_in_set(y) + @test_throws( + ErrorException("VariableInSetRef does not exist for $y"), + VariableInSetRef(y), + ) + for set in ( + MOI.SecondOrderCone(3), + MOI.RotatedSecondOrderCone(3), + MOI.ExponentialCone(), + ) + model = Model() + @variable(model, x[1:3] in set) + @test has_variable_in_set(x) + c = VariableInSetRef(x) + o = constraint_object(c) + @test isequal_canonical(reshape_vector(o.func, o.shape), x) + @test o.set == set + end + return +end + +function test_variable_in_set_PSD() + model = Model() + @variable(model, x[1:2, 1:2], PSD) + @test has_variable_in_set(x) + c = VariableInSetRef(x) + o = constraint_object(c) + @test isequal_canonical(reshape_vector(o.func, o.shape), x) + @test o.set == MOI.PositiveSemidefiniteConeTriangle(2) + return +end + +function test_variable_in_set_HermitianPSDCone() + model = Model() + @variable(model, x[1:2, 1:2] in HermitianPSDCone()) + @test has_variable_in_set(x) + c = VariableInSetRef(x) + o = constraint_object(c) + @test isequal_canonical(reshape_vector(o.func, o.shape), x) + @test o.set == MOI.HermitianPositiveSemidefiniteConeTriangle(2) + return +end + end # module TestVariable From e3166d1d9280cd6e2ed3fb5edf726f82830bf6f1 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Feb 2025 15:56:36 +1300 Subject: [PATCH 2/5] Update --- src/variables.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index cf9b4b15476..0743286dc2c 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -2406,10 +2406,11 @@ function add_variable( vectorize(names, variable.shape), T, ) - var_refs = [GenericVariableRef{T}(model, xi) for xi in x] - ret = reshape_vector(var_refs, variable.shape) - model.variable_in_set_ref[ret] = ci - if !(variable.shape isa ScalarShape) && !(variable.shape isa VectorShape) + ret = reshape_vector(GenericVariableRef{T}.(model, x), variable.shape) + if ci !== nothing # ci === nothing if variable.set isa MOI.Reals + model.variable_in_set_ref[ret] = ci + end + if variable.shape != ScalarShape() && variable.shape != VectorShape() model.shapes[ci] = variable.shape end return ret From 293d660d24c16ba1b9815a3d7c95beffca358a48 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 28 Feb 2025 19:36:17 +1300 Subject: [PATCH 3/5] Update variables.jl --- src/variables.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index 0743286dc2c..adb94cb21ba 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -2409,9 +2409,9 @@ function add_variable( ret = reshape_vector(GenericVariableRef{T}.(model, x), variable.shape) if ci !== nothing # ci === nothing if variable.set isa MOI.Reals model.variable_in_set_ref[ret] = ci - end - if variable.shape != ScalarShape() && variable.shape != VectorShape() - model.shapes[ci] = variable.shape + if variable.shape != ScalarShape() && variable.shape != VectorShape() + model.shapes[ci] = variable.shape + end end return ret end From f4d2ebc037055b3f0e37246e2907a8bac3ad822b Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 28 Feb 2025 20:09:09 +1300 Subject: [PATCH 4/5] Update --- docs/src/manual/variables.md | 17 +++++++++++++++-- src/sd.jl | 9 +++++---- test/test_variable.jl | 5 +---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/docs/src/manual/variables.md b/docs/src/manual/variables.md index 627a526cbcb..6d49ec479a7 100644 --- a/docs/src/manual/variables.md +++ b/docs/src/manual/variables.md @@ -1418,8 +1418,21 @@ julia> @variable(model, H[1:2, 1:2] in HermitianPSDCone()) This adds 4 real variables in the [`MOI.HermitianPositiveSemidefiniteConeTriangle`](@ref): ```jldoctest hermitian_psd -julia> first(all_constraints(model, Vector{VariableRef}, MOI.HermitianPositiveSemidefiniteConeTriangle)) -[real(H[1,1]), real(H[1,2]), real(H[2,2]), imag(H[1,2])] ∈ MathOptInterface.HermitianPositiveSemidefiniteConeTriangle(2) +julia> c = VariableInSetRef(H) +[real(H[1,1]) real(H[1,2]) + imag(H[1,2]) im + real(H[1,2]) - imag(H[1,2]) im real(H[2,2])] ∈ HermitianPSDCone() + +julia> o = constraint_object(c); + +julia> o.func +4-element Vector{VariableRef}: + real(H[1,1]) + real(H[1,2]) + real(H[2,2]) + imag(H[1,2]) + +julia> o.set +MathOptInterface.HermitianPositiveSemidefiniteConeTriangle(2) ``` ### Example: Hermitian variables diff --git a/src/sd.jl b/src/sd.jl index f07b1a09d65..a2f87811468 100644 --- a/src/sd.jl +++ b/src/sd.jl @@ -477,7 +477,9 @@ julia> @variable(model, H[1:3, 1:3] in HermitianPSDCone()) real(H[1,2]) - imag(H[1,2]) im real(H[2,3]) + imag(H[2,3]) im real(H[1,3]) - imag(H[1,3]) im real(H[3,3]) -julia> all_variables(model) +julia> c = constraint_object(VariableInSetRef(H)); + +julia> c.func 9-element Vector{VariableRef}: real(H[1,1]) real(H[1,2]) @@ -489,9 +491,8 @@ julia> all_variables(model) imag(H[1,3]) imag(H[2,3]) -julia> all_constraints(model, Vector{VariableRef}, MOI.HermitianPositiveSemidefiniteConeTriangle) -1-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.VectorOfVariables, MathOptInterface.HermitianPositiveSemidefiniteConeTriangle}}}: - [real(H[1,1]), real(H[1,2]), real(H[2,2]), real(H[1,3]), real(H[2,3]), real(H[3,3]), imag(H[1,2]), imag(H[1,3]), imag(H[2,3])] ∈ MathOptInterface.HermitianPositiveSemidefiniteConeTriangle(3) +julia> c.set +MathOptInterface.HermitianPositiveSemidefiniteConeTriangle(3) ``` We see in the output of the last commands that 9 real variables were created. The matrix `H` constrains affine expressions in terms of these 9 variables that diff --git a/test/test_variable.jl b/test/test_variable.jl index 3aa3bfb5caf..a07f1b803f0 100644 --- a/test/test_variable.jl +++ b/test/test_variable.jl @@ -1734,10 +1734,7 @@ end function test_variable_in_set_vector() model = Model() - @variable(model, x, set = MOI.Integer()) - c = VariableInSetRef(x) - @test has_variable_in_set(x) - @variable(model, y, Int) + @variable(model, y[1:2], Int) @test !has_variable_in_set(y) @test_throws( ErrorException("VariableInSetRef does not exist for $y"), From b6db5658fe71124bfe20d5763dc97c6c493ce36d Mon Sep 17 00:00:00 2001 From: odow Date: Mon, 3 Mar 2025 10:16:58 +1300 Subject: [PATCH 5/5] Update --- src/variables.jl | 4 ++-- test/test_variable.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/variables.jl b/src/variables.jl index adb94cb21ba..a8bf06cb6db 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -1920,7 +1920,7 @@ function VariableInSetRef(x::AbstractJuMPScalar) model = owner_model(x) ci = get(model.variable_in_set_ref, x, nothing) if ci === nothing - error("VariableInSetRef does not exist for $x") + error("`VariableInSetRef` does not exist for `$x`") end return constraint_ref_with_index(model, ci) end @@ -1929,7 +1929,7 @@ function VariableInSetRef(x::AbstractArray{<:AbstractJuMPScalar}) model = owner_model(first(x)) ci = get(model.variable_in_set_ref, x, nothing) if ci === nothing - error("VariableInSetRef does not exist for $x") + error("`VariableInSetRef` does not exist for `$x`") end return constraint_ref_with_index(model, ci) end diff --git a/test/test_variable.jl b/test/test_variable.jl index a07f1b803f0..0045f45c93c 100644 --- a/test/test_variable.jl +++ b/test/test_variable.jl @@ -1708,7 +1708,7 @@ function test_variable_in_set_scalar() @variable(model, y, Int) @test !has_variable_in_set(y) @test_throws( - ErrorException("VariableInSetRef does not exist for $y"), + ErrorException("`VariableInSetRef` does not exist for `$y`"), VariableInSetRef(y), ) for set in ( @@ -1737,7 +1737,7 @@ function test_variable_in_set_vector() @variable(model, y[1:2], Int) @test !has_variable_in_set(y) @test_throws( - ErrorException("VariableInSetRef does not exist for $y"), + ErrorException("`VariableInSetRef` does not exist for `$y`"), VariableInSetRef(y), ) for set in (