From 908275f105d7902d0065aad8a56a2caf80e391cf Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 1 Nov 2023 08:16:04 +1300 Subject: [PATCH] Add start_value, lower_bound, and upper_bound support for some GenericAffExpr (#3551) --- docs/src/manual/variables.md | 19 ---------- src/aff_expr.jl | 58 ++++++++++++++++++++++++++++++ test/test_expr.jl | 68 ++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 19 deletions(-) diff --git a/docs/src/manual/variables.md b/docs/src/manual/variables.md index aa69b829841..ce8d6cf11a5 100644 --- a/docs/src/manual/variables.md +++ b/docs/src/manual/variables.md @@ -1214,25 +1214,6 @@ julia> @variable(model, x[1:2, 1:2] in SkewSymmetricMatrixSpace()) `model`; the remaining elements in `x` are linear transformations of the single variable. -Because the returned matrix `x` is `Matrix{AffExpr}`, you cannot use -variable-related functions on its elements: -```jldoctest skewsymmetric -julia> set_lower_bound(x[1, 2], 0.0) -ERROR: MethodError: no method matching set_lower_bound(::AffExpr, ::Float64) -[...] -``` - -Instead, you can convert an upper-triangular elements to a variable as follows: -```jldoctest skewsymmetric -julia> to_variable(x::AffExpr) = first(keys(x.terms)) -to_variable (generic function with 1 method) - -julia> to_variable(x[1, 2]) -x[1,2] - -julia> set_lower_bound(to_variable(x[1, 2]), 0.0) -``` - ### Example: Hermitian positive semidefinite variables Declare a matrix of JuMP variables to be Hermitian positive semidefinite using diff --git a/src/aff_expr.jl b/src/aff_expr.jl index f6b3db65361..86570a9ad2b 100644 --- a/src/aff_expr.jl +++ b/src/aff_expr.jl @@ -809,3 +809,61 @@ moi_function(a::Vector{<:GenericAffExpr}) = MOI.VectorAffineFunction(a) function moi_function_type(::Type{<:Vector{<:GenericAffExpr{T}}}) where {T} return MOI.VectorAffineFunction{T} end + +""" + _eval_as_variable(f::F, x::GenericAffExpr, args...) where {F} + +In many cases, `@variable` can return a `GenericAffExpr` instead of a +`GenericVariableRef`. This is particularly the case for complex-valued +expressions. To make common operatons like `lower_bound(x)` work, we should +forward the method if and only if `x` is convertable to a `GenericVariableRef`. +""" +function _eval_as_variable(f::F, x::GenericAffExpr, args...) where {F} + if length(x.terms) != 1 + error( + "Cannot call $f with $x because it is not an affine expression " * + "of one variable.", + ) + end + variable, coefficient = first(x.terms) + if !isone(coefficient) + error( + "Cannot call $f with $x because the variable has a coefficient " * + "that is different to `+1`.", + ) + end + return f(variable, args...) +end + +# start_value(::GenericAffExpr) + +start_value(x::GenericAffExpr) = _eval_as_variable(start_value, x) + +function set_start_value(x::GenericAffExpr, value) + _eval_as_variable(set_start_value, x, value) + return +end + +# lower_bound(::GenericAffExpr) + +has_lower_bound(x::GenericAffExpr) = _eval_as_variable(has_lower_bound, x) + +lower_bound(x::GenericAffExpr) = _eval_as_variable(lower_bound, x) + +delete_lower_bound(x::GenericAffExpr) = _eval_as_variable(delete_lower_bound, x) + +function set_lower_bound(x::GenericAffExpr, value) + return _eval_as_variable(set_lower_bound, x, value) +end + +# upper_bound(::GenericAffExpr) + +has_upper_bound(x::GenericAffExpr) = _eval_as_variable(has_upper_bound, x) + +upper_bound(x::GenericAffExpr) = _eval_as_variable(upper_bound, x) + +delete_upper_bound(x::GenericAffExpr) = _eval_as_variable(delete_upper_bound, x) + +function set_upper_bound(x::GenericAffExpr, value) + return _eval_as_variable(set_upper_bound, x, value) +end diff --git a/test/test_expr.jl b/test/test_expr.jl index 1aad5b4fb0c..1a446493496 100644 --- a/test/test_expr.jl +++ b/test/test_expr.jl @@ -451,4 +451,72 @@ function test_quadexpr_owner_model() return end +function test_aff_expr_complex_lower_bound() + model = Model() + @variable(model, x in ComplexPlane()) + y = real(x) + @test !has_lower_bound(y) + set_lower_bound(y, 1) + @test has_lower_bound(y) + @test lower_bound(y) == 1 + delete_lower_bound(y) + @test !has_lower_bound(y) + return +end + +function test_aff_expr_complex_upper_bound() + model = Model() + @variable(model, x in ComplexPlane()) + y = real(x) + @test !has_upper_bound(y) + set_upper_bound(y, 1) + @test has_upper_bound(y) + @test upper_bound(y) == 1 + delete_upper_bound(y) + @test !has_upper_bound(y) + return +end + +function test_aff_expr_complex_start_value() + model = Model() + @variable(model, x in ComplexPlane()) + y = real(x) + @test start_value(y) === nothing + set_start_value(y, 1) + @test start_value(y) == 1 + return +end + +function test_aff_expr_complex_HermitianPSDCone() + model = Model() + @variable(model, x[1:2, 1:2] in HermitianPSDCone()) + @test start_value(x[1, 1]) === nothing + set_lower_bound(x[1, 1], 2.5) + @test has_lower_bound(x[1, 1]) + @test lower_bound(x[1, 1]) == 2.5 + @test_throws( + ErrorException( + "Cannot call $start_value with $(x[2, 1]) because it is not an affine " * + "expression of one variable.", + ), + start_value(x[2, 1]), + ) + @test_throws( + ErrorException( + "Cannot call $start_value with $(imag(x[2, 1])) because the " * + "variable has a coefficient that is different to `+1`.", + ), + start_value(imag(x[2, 1])), + ) + y = AffExpr(0.0) + @test_throws( + ErrorException( + "Cannot call $start_value with $y because it is not an affine " * + "expression of one variable.", + ), + start_value(y), + ) + return +end + end # TestExpr