Error when constraining variable to be Hermitian PSD

I’m writing an SDP for a traditional problem in quantum information, state discrimination. When I constrain the variables to real symmetric, it works. But the problem really needs them to be complex Hermitian. The code is

using JuMP
import LinearAlgebra
import Random
import SCS

function random_state(d)
	x = randn(ComplexF64, (d,d))
	y = x*adjoint(x)
	rho = Hermitian(y/tr(y))
	return rho
end
	
function discriminate(d,n)
    model = Model(SCS.Optimizer)
    rho = Array{ComplexF64}(undef,d, d, n)
    for i=1:n
    	rho[:,:,i] = random_state(d)
    end
	E = model[:E] = reshape(
           hcat([
               @variable(model, [1:d, 1:d] in PSDCone(), base_name = "E$(i)")
               for i in 1:n-1
           ]...),
           d, d, n-1,
       )
	lastE = I - sum(E[:,:,i] for i in 1:n-1)
	@constraint(model, lastE in PSDCone())
	obj = real(sum(dot(rho[:,:,i],E[:,:,i]) for i in 1:n-1)+dot(rho[:,:,n],lastE))/n
    @objective(model, Max, obj)
    optimize!(model)
    print(value(obj))
end	

When I change “PSDCone()” to “HermitianPSDCone()” I get the error “Unrecognized constraint building format.” Is this a bug? Is there a way to make it work?

A bit unrelated, but is there a more convenient way to declare a set of Hermitian variables? With YALMIP I can just do

E = sdpvar(d,d,n-1,'hermitian','complex');

which is much more concise and readable.

I should improve the documentation, Complex number support · JuMP, to explain that H needs to be explicitly marked as Hermitian. (See [docs] clarify that matrix must be Hermitian by odow · Pull Request #3241 · jump-dev/JuMP.jl · GitHub)

Here’s how I would write your model:

using JuMP
import LinearAlgebra
import SCS

function random_state(d)
    x = randn(ComplexF64, (d, d))
    y = x * x'
    return LinearAlgebra.Hermitian(y / LinearAlgebra.tr(y))
end
    
function discriminate(d, n)
    model = Model(SCS.Optimizer)
    rho = [random_state(d) for i in 1:n]
    E = [
        @variable(model, [1:d, 1:d] in HermitianPSDCone(), base_name = "E$(i)")
        for i in 1:n-1
    ]
    lastE = LinearAlgebra.I - sum(E)
    @constraint(model, LinearAlgebra.Hermitian(lastE) in HermitianPSDCone())
    obj = real(
        sum(LinearAlgebra.dot(rho[i], E[i]) for i in 1:n-1) +
        LinearAlgebra.dot(rho[n], lastE)
    ) / n
    @objective(model, Max, obj)
    optimize!(model)
    return objective_value(model)
end

Thanks a lot! Something very weird happened, though. If instead of your version

	lastE = I - sum(E[i] for i in 1:n-1)
	@constraint(model, Hermitian(lastE) in HermitianPSDCone())

I write

	lastE = Hermitian(I - sum(E[i] for i in 1:n-1))
	@constraint(model, lastE in HermitianPSDCone())

I don’t get an error message either, but the SDP gives me the wrong result! Any idea what is going on?

And, since you’re improving the documentation anyway, perhaps it would be worth mentioning this much nicer way of declaring a set of Hermitian PSD variables that you wrote down?

Hmm. Let me take a look.

You could also write it like this, which is perhaps even simpler:

function discriminate(d, n)
    model = Model(SCS.Optimizer)
    set_silent(model)
    rho = [random_state(d) for i in 1:n]
    E = [@variable(model, [1:d, 1:d] in HermitianPSDCone()) for i in 1:n]
    @constraint(model, sum(E) .== LinearAlgebra.I)
    @objective(model, Max, real(LinearAlgebra.dot(rho, E)) / n)
    optimize!(model)
    return objective_value(model)
end

I’m aware of this formulation, it’s the most straightforward way to code state discrimination. I avoided it because it has unnecessary variables and constraints, and as such is slower and less precise

1 Like

It looks like your incorrect case is a bug in JuMP: Incorrect summation with Hermitian matrices · Issue #3242 · jump-dev/JuMP.jl · GitHub

We’ve only recently added support for HermitianPSDCone and the associated Complex support, but this is a pretty major nasty on our part. It took me a while to find!

cc @blegat

Wow, you tracked down and fixed the bug on a Sunday. That’s some amazing dedication, thanks!

May I suggest to add this code as an example in the documentation for complex numbers? There are no examples there, and this is a simple and well-known problem.

1 Like

Is there an accessible reference one could link to for this?

For @odow, it was already Monday in New-Zealand ^^
@araujoms Can you check that your model now works with JuMP#master ?
I agree it would make a nice tutorial, quantum information was one of the motivation for adding Complex number support. A reference would be helpful indeed.

1 Like

There’s always Wikipedia, but that article is not so nice. A better reference would be this review or Section 11.3 of Bergou’s book, that I tried to link but got blocked by the spam filter.

1 Like

Just tested with the git version, now the following code works:

function discriminate(d,n)
    model = Model(SCS.Optimizer)
    rho = [random_state(d) for i in 1:n]
	E = [@variable(model, [1:d, 1:d] in HermitianPSDCone()) for i in 1:n-1]
	lastE = Hermitian(I - sum(E[i] for i in 1:n-1))
	@constraint(model, lastE in HermitianPSDCone())
	obj = real(sum(dot(rho[i],E[i]) for i in 1:n-1)+dot(rho[n],lastE))/n
    @objective(model, Max, obj)
    optimize!(model)
    print(value(obj))
end	

Thanks for the quick reply, this should be fixed in JuMP v1.8.2

@araujoms I’ve started a tutorial, but I’m not a domain expert so any comments/suggestions would be helpful: