Bug : input list of operators gets modified

Hi everyone,

I have a list of operators, that I would like to tensor with an additional one, which I am doing this way :

list_in = [qml.PauliZ(0),qml.PauliZ(1),qml.PauliZ(0)@qml.PauliZ(1)]
add_obs = qml.PauliZ(2)
list_out = [obs @ add_obs  for obs  in list_in]

This works perfectly and I am getting the output I expect :

[PauliZ(wires=[0]) @ PauliZ(wires=[2]),
 PauliZ(wires=[1]) @ PauliZ(wires=[2]),
 PauliZ(wires=[0]) @ PauliZ(wires=[1]) @ PauliZ(wires=[2])]

However that also modifies the original input, adding this operator only on any input that already has an @ in it:

 PauliZ(wires=[0]) @ PauliZ(wires=[1]) @ PauliZ(wires=[2])]

I have tried using list_in .copy() instead and it doesn’t work, moreover it doesn’t happen to all inputs (single pauli are fine eg [Z0,Z1])

Would anyone be able to explain this behavior, and maybe how to avoid this ?

My pennylane version is 0.23.1

Hi @alicewithoutbob,

If you upgrade to v0.25 or greater, you can use qml.prod. This new way of multiplying operators always creates a new operator instead of updating one in place, like Tensor does.

list_in = [qml.PauliZ(0),qml.PauliZ(1), qml.prod(qml.PauliZ(0), qml.PauliZ(1))]
add_obs = qml.PauliZ(2)
list_out = [qml.prod(obs, add_obs)  for obs  in list_in]
>>> list_out
[PauliZ(wires=[0]) @ PauliZ(wires=[2]),
 PauliZ(wires=[1]) @ PauliZ(wires=[2]),
 (PauliZ(wires=[0]) @ PauliZ(wires=[1])) @ PauliZ(wires=[2])]
>>> list_in
[PauliZ(wires=[0]), PauliZ(wires=[1]), PauliZ(wires=[0]) @ PauliZ(wires=[1])]
1 Like

Thank you @christina for your answer, but the output is not a Pauli word (because of the parenthesis ?)

(PauliZ(wires=[0]) @ PauliZ(wires=[1])) @ PauliZ(wires=[2]) 

And when I try to evaluate the expectation value I get :

 Expected a Pauli word Observable instance, instead got expval(PauliZ(wires=[0])) @ expval(PauliZ(wires=[2]))

There is a difference of results between the two following pieces of code. Is there a practical reason for this ?

(PauliZ(wires=[0]) @ PauliZ(wires=[1])) @ PauliZ(wires=[2])

PauliZ(wires=[0]) @ PauliZ(wires=[1]) @ PauliZ(wires=[2])

What are you using to take an expectation value? Mind sharing the code you used it with?

The new classes Prod, Sum, and SProd are plain Operator's, not Observable's, but they should still work with expectation values and variances on default qubit and lightning qubit.

And yes, performing qml.prod(qml.prod(qml.PauliZ(0),qml.PauliZ(1)),qml.PauliZ(2)) will give you a product of a product and an operator. This will be represented internally differently, but it should end up giving you the same results. Please let us know if they are giving different behavior for things like expectation values, matrices, eigenvalues, etc, as that should not be happening.

You can use qml.simplify(ops) to combine all products together and remove the nested structure.

thanks @christina for your answer.

I think I have identified a strange behaviour.

I have the following function that allows me to return the expectation value for a list of commuting observables :

def mesure_obs(q_fun,nqbits,obs):
    dev = qml.device("lightning.qubit", wires=range(nqbits)) 
    def circuit_(*args):
        return [qml.expval(o) for o in obs]
    return circuit_

Now I am seemingly contructing the same list of observables in two ways, first with the contruction we discussed previously :

obs = [qml.PauliZ(0),qml.PauliZ(1),qml.PauliZ(0)@qml.PauliZ(1)]
P = qml.PauliZ(2)
obs_cond = obs.copy() + [qml.prod(o , P) for o in obs] + [P]
>>>PauliZ(wires=[0]) @ PauliZ(wires=[1]),
>>>PauliZ(wires=[0]) @ PauliZ(wires=[2]),
>>>PauliZ(wires=[1]) @ PauliZ(wires=[2]),
>>>(PauliZ(wires=[0]) @ PauliZ(wires=[1])) @ PauliZ(wires=[2]),

and second by simply writing the result down directly, which results exaclty in the same thing when printing them :

obs_cond_bis = [qml.PauliZ(wires=[0]),
 qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1]),
 qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[2]),
 qml.PauliZ(wires=[1]) @ qml.PauliZ(wires=[2]),
 (qml.PauliZ(wires=[0]) @ qml.PauliZ(wires=[1])) @ qml.PauliZ(wires=[2]),

Now trying to apply that a quantum function c (I dont think whats inside matter I have tried several and the result was the same everytime)

def c():

And the two seemingly identical lists behave differently, one returns the correct result while the other returns an error.

mesure_obs(c,3, obs_cond_bis)()
>>>tensor([0., 0., 1., 0., 0., 1., 1.], requires_grad=True)

mesure_obs(c,3, obs_cond)()
>>>TypeError: Expected a Pauli word Observable instance, instead got expval(PauliZ(wires=[0])) @ expval(PauliZ(wires=[2])).

But on the other hand doing the following works :

for o in obs_cond:
    print(mesure_obs(c,3, [o])())

Am I misusing a functionality of pennylane ?

going down to the bottom of this it seems that the problem is that the output of prod is not a Pauli word :

qml.grouping.is_pauli_word(qml.prod(qml.PauliX(0) , qml.PauliZ(1)))

qml.grouping.is_pauli_word(qml.PauliX(0) @ qml.PauliZ(1))

And therefore all subsequent operations assuming it is one fail.

Thanks for your code @alicewithoutbob :+1:

You have indeed stumbled across a bug. I’ll create an issue on the repository, and we’ll try to get a fix in as soon as we can. It should come out in the next release.

The problem occurs when more than one expectation value occurs at the same time. The pipeline to separate out the observables into things that can simultaneously measured has no yet been updated to work with the new type of observables.

You already have the two work arounds that you can use until we get in the bug fix:

  • Continue to use Tensor, but just copy it
  • measure at most one observable at a time.

thank you @christina, I have managed to find a work around using string representations of Pauli strings (e.g. 'IXXZ'), and manipulating these instead.

Thank you for sharing your solution @alicewithoutbob!