From 51056e7d168d634b2432ba8435a1ab0fb4c47e0e Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 6 Dec 2022 10:23:08 +1300 Subject: [PATCH 1/4] [docs] add documentation for complex-number support --- docs/make.jl | 1 + docs/src/manual/complex.md | 241 +++++++++++++++++++++++++++++++++++++ src/aff_expr.jl | 5 +- 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 docs/src/manual/complex.md diff --git a/docs/make.jl b/docs/make.jl index b4c591c4f1e..74bba578611 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -183,6 +183,7 @@ const _PAGES = [ "manual/solutions.md", "manual/nlp.md", "manual/callbacks.md", + "manual/complex.md", ], "API Reference" => [ "reference/models.md", diff --git a/docs/src/manual/complex.md b/docs/src/manual/complex.md new file mode 100644 index 00000000000..4928ff16c71 --- /dev/null +++ b/docs/src/manual/complex.md @@ -0,0 +1,241 @@ +```@meta +CurrentModule = JuMP +DocTestSetup = quote + using JuMP + import HiGHS +end +DocTestFilters = [r"≤|<=", r"≥|>=", r" == | = ", r" ∈ | in ", r"MathOptInterface|MOI"] +``` + +# Complex number support + +JuMP has a limited range of support for complex-valued variables and +constraints. Since no current solvers natively support complex-valued variables +and constraints, JuMP reformulates all complex expressions into their real and +imaginary parts. + +## Complex-valued variables + +Create a complex-valued variable using [`ComplexPlane`](@ref): + +```jldoctest complex_variables +julia> model = Model(); + +julia> @variable(model, x in ComplexPlane()) +real(x) + (0.0 + 1.0im) imag(x) +``` + +Note that `x` is not a [`VariableRef`](@ref); instead, it is an affine +expression with `Complex{Float64}`-valued coefficients: + +```jldoctest complex_variables +julia> typeof(x) +GenericAffExpr{ComplexF64, VariableRef} +``` + +Behind the scenes, JuMP has created two real-valued variables, with names +`"real(x)"` and `"imag(x)"`: + +```jldoctest complex_variables +julia> all_variables(model) +2-element Vector{VariableRef}: + real(x) + imag(x) + +julia> name.(all_variables(model)) +2-element Vector{String}: + "real(x)" + "imag(x)" +``` + +Use the `real` and `imag` functions on `x` to return a real-valued affine +expression representing each variable: + +```jldoctest complex_variables +julia> typeof(real(x)) +AffExpr (alias for GenericAffExpr{Float64, VariableRef}) + +julia> typeof(imag(x)) +AffExpr (alias for GenericAffExpr{Float64, VariableRef}) +``` + +To create an anonymous variable, use the `set` keyword argument: + +```jldoctest +julia> model = Model(); + +julia> x = @variable(model, set = ComplexPlane()) +_[1] + (0.0 + 1.0im) _[2] +``` + +## Complex-valued variable bounds + +Because complex-valued variables lack a total ordering, the definition of a +variable bound for a complex-valued variable is ambiguous. If you pass a real- +or complex-valued argument to keywords such as `lower_bound`, `upper_bound`, +and `start_value`, JuMP will apply the real and imaginary parts to the +associated real-valued variables. + +```jldoctest complex_variables +julia> model = Model(); + +julia> @variable( + model, + x in ComplexPlane(), + lower_bound = 1.0, + upper_bound = 2.0 + 3.0im, + start = 4im, + ) +real(x) + (0.0 + 1.0im) imag(x) + +julia> vars = all_variables(model) +2-element Vector{VariableRef}: + real(x) + imag(x) + +julia> lower_bound.(vars) +2-element Vector{Float64}: + 1.0 + 0.0 + +julia> upper_bound.(vars) +2-element Vector{Float64}: + 2.0 + 3.0 + +julia> start_value.(vars) +2-element Vector{Float64}: + 0.0 + 4.0 +``` + +## Complex-valued equality constraints + +JuMP reformulates complex-valued equality constraints into two real-valued +constraints: one representing the real part, and one representing the imaginary +part. Thus, complex-valued equality constraints can be solved any solver that +supports the real-valued constraint type. + +For example: + +```jldoctest +julia> model = Model(HiGHS.Optimizer); + +julia> set_silent(model) + +julia> @variable(model, x[1:2]); + +julia> @constraint(model, (1 + 2im) * x[1] + 3 * x[2] == 4 + 5im) +(1.0 + 2.0im) x[1] + (3.0 + 0.0im) x[2] = 4.0 + 5.0im + +julia> optimize!(model) + +julia> value.(x) +2-element Vector{Float64}: + 2.5 + 0.5 +``` + +is equivalent to + +```jldoctest +julia> model = Model(HiGHS.Optimizer); + +julia> set_silent(model) + +julia> @variable(model, x[1:2]); + +julia> @constraint(model, 1 * x[1] + 3 * x[2] == 4) # real component +x[1] + 3 x[2] = 4.0 + +julia> @constraint(model, 2 * x[1] == 5) # imag component +2 x[1] = 5.0 + +julia> optimize!(model) + +julia> value.(x) +2-element Vector{Float64}: + 2.5 + 0.5 +``` + +This also applies if the variables are complex-valued: + +```jldoctest +julia> model = Model(HiGHS.Optimizer); + +julia> set_silent(model) + +julia> @variable(model, x in ComplexPlane()); + +julia> @constraint(model, (1 + 2im) * x + 3 * x == 4 + 5im) +(4.0 + 2.0im) real(x) + (-2.0 + 4.0im) imag(x) = 4.0 + 5.0im + +julia> optimize!(model) + +julia> value(x) +1.3 + 0.6000000000000001im +``` + +which is equivalent to + +```jldoctest +julia> model = Model(HiGHS.Optimizer); + +julia> set_silent(model) + +julia> @variable(model, x_real); + +julia> @variable(model, x_imag); + +julia> @constraint(model, x_real - 2 * x_imag + 3 * x_real == 4) +4 x_real - 2 x_imag = 4.0 + +julia> @constraint(model, x_imag + 2 * x_real + 3 * x_imag == 5) +2 x_real + 4 x_imag = 5.0 + +julia> optimize!(model) + +julia> value(x_real) + value(x_imag) * im +1.3 + 0.6000000000000001im +``` + +## Hermitian PSD Cones + +JuMP supports creating matrices where are Hermitian. +```jldoctest hermitian_psd_cone +julia> model = Model(); + +julia> @variable(model, H[1:3, 1:3] in HermitianPSDCone()) +3×3 Matrix{GenericAffExpr{ComplexF64, VariableRef}}: + real(H[1,1]) … real(H[1,3]) + (0.0 + 1.0im) imag(H[1,3]) + real(H[1,2]) + (-0.0 - 1.0im) imag(H[1,2]) real(H[2,3]) + (0.0 + 1.0im) imag(H[2,3]) + real(H[1,3]) + (-0.0 - 1.0im) imag(H[1,3]) real(H[3,3]) +``` + +Behind the scenes, JuMP has created nine real-valued decision variables: + +```jldoctest hermitian_psd_cone +julia> all_variables(model) +9-element Vector{VariableRef}: + 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]) +``` + +Each element of `H` is an affine expression with `Complex{Float64}`-valued +coefficients: + +```jldoctest hermitian_psd_cone +julia> typeof(H[1, 1]) +GenericAffExpr{ComplexF64, VariableRef} + +julia> typeof(H[2, 1]) +GenericAffExpr{ComplexF64, VariableRef} +``` diff --git a/src/aff_expr.jl b/src/aff_expr.jl index 26a579cedfa..56b7e561ec0 100644 --- a/src/aff_expr.jl +++ b/src/aff_expr.jl @@ -211,7 +211,10 @@ Base.conj(a::GenericAffExpr{<:Complex}) = map_coefficients(conj, a) function _map_coefs(f::Function, a::GenericAffExpr{Complex{T},V}) where {T,V} output = convert(GenericAffExpr{T,V}, f(a.constant)) for (coef, var) in linear_terms(a) - output.terms[var] = f(coef) + new_coef = f(coef) + if !iszero(new_coef) + output.terms[var] = new_coef + end end return output end From 2a0079aae80125b06769603c6b38a1a014d1e2bb Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 6 Dec 2022 10:44:56 +1300 Subject: [PATCH 2/4] Update --- docs/src/manual/complex.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/src/manual/complex.md b/docs/src/manual/complex.md index 4928ff16c71..c6e167fe1b9 100644 --- a/docs/src/manual/complex.md +++ b/docs/src/manual/complex.md @@ -229,6 +229,18 @@ julia> all_variables(model) imag(H[2,3]) ``` +and a `Vector{VariableRef}-in-MOI.HermitianPositiveSemidefiniteConeTriangle` +constraint: + +```jldoctest hermitian_psd_conne +julia> num_constraints(model, Vector{VariableRef}, MOI.HermitianPositiveSemidefiniteConeTriangle) +1 +``` + +The [`MOI.HermitianPositiveSemidefiniteConeTriangle`](@ref) set can be +efficiently bridged to [`MOI.PositiveSemidefiniteConeTriangle`](@ref), so it can +be solved by any solver that supports PSD constraints. + Each element of `H` is an affine expression with `Complex{Float64}`-valued coefficients: From 07c83c1614c641eab8d50254cd29d67b44ba3e13 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 6 Dec 2022 12:18:03 +1300 Subject: [PATCH 3/4] Fix typo --- docs/src/manual/complex.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/manual/complex.md b/docs/src/manual/complex.md index c6e167fe1b9..757dd7fc659 100644 --- a/docs/src/manual/complex.md +++ b/docs/src/manual/complex.md @@ -232,7 +232,7 @@ julia> all_variables(model) and a `Vector{VariableRef}-in-MOI.HermitianPositiveSemidefiniteConeTriangle` constraint: -```jldoctest hermitian_psd_conne +```jldoctest hermitian_psd_cone julia> num_constraints(model, Vector{VariableRef}, MOI.HermitianPositiveSemidefiniteConeTriangle) 1 ``` From b6b8b337effa995623089e053b35993952d38c8e Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 7 Dec 2022 07:56:28 +1300 Subject: [PATCH 4/4] Add printing test --- test/complex.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/complex.jl b/test/complex.jl index e156bbd4472..f8e0a4b6471 100644 --- a/test/complex.jl +++ b/test/complex.jl @@ -160,6 +160,14 @@ function test_complex_print() return end +function test_complex_print_zeros() + model = Model() + @variable(model, x in ComplexPlane()) + @test sprint(show, real(x)) == "real(x)" + @test sprint(show, imag(x)) == "imag(x)" + return +end + function test_complex_conj() model = Model() @variable(model, x)