Skip to content

Commit

Permalink
Add start_value, lower_bound, and upper_bound support for some Generi…
Browse files Browse the repository at this point in the history
…cAffExpr (#3551)
  • Loading branch information
odow authored Oct 31, 2023
1 parent 35e7713 commit 908275f
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 19 deletions.
19 changes: 0 additions & 19 deletions docs/src/manual/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions src/aff_expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
68 changes: 68 additions & 0 deletions test/test_expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 908275f

Please sign in to comment.