Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add start_value, lower_bound, and upper_bound support for GenericAffExpr #3551

Merged
merged 7 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading