PennyLaneAI / pennylane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
https://pennylane.ai
Apache License 2.0
2.35k stars 603 forks source link

`qml.Identity` doesn't apply to multiple wires when multiplied by a scalar [BUG] #3646

Closed isaacdevlugt closed 1 year ago

isaacdevlugt commented 1 year ago

Expected behavior

The Identity operator should apply to all specified wires.

>>> num_wires = 3
>>> ham = 2.0 * qml.Identity(wires=range(num_wires))
>>> ham
<Hamiltonian: terms=1, wires=[0]>
>>> ham.coeffs, ham.ops
(array([2.]), [Identity(wires=[0, 1, 2])])

Actual behavior

It doesn't apply to all specified wires.

>>> num_wires = 3
>>> ham = 2.0 * qml.Identity(wires=range(num_wires))
>>> ham
<Hamiltonian: terms=1, wires=[0]>
>>> ham.coeffs, ham.ops
(array([2.]), [Identity(wires=[0])])

Additional information

No response

Source code

No response

Tracebacks

No response

System information

Name: PennyLane
Version: 0.28.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /opt/anaconda3/envs/xanadu/lib/python3.8/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, retworkx, scipy, semantic-version, toml
Required-by: PennyLane-Lightning

Platform info:           macOS-10.16-x86_64-i386-64bit
Python version:          3.8.14
Numpy version:           1.23.0
Scipy version:           1.10.0
Installed devices:
- lightning.qubit (PennyLane-Lightning-0.28.0)
- default.gaussian (PennyLane-0.28.0)
- default.mixed (PennyLane-0.28.0)
- default.qubit (PennyLane-0.28.0)
- default.qubit.autograd (PennyLane-0.28.0)
- default.qubit.jax (PennyLane-0.28.0)
- default.qubit.tf (PennyLane-0.28.0)
- default.qubit.torch (PennyLane-0.28.0)
- default.qutrit (PennyLane-0.28.0)
- null.qubit (PennyLane-0.28.0)

Existing GitHub issues

albi3ro commented 1 year ago

So what's happening is that simplification happens immediately during construction via dunder methods.

To see some more examples or this, consider:

>>> 2 * qml.Identity((0,1,2)) @ qml.PauliX(3)
  (2) [X3]

Since it's simplified on construction, the identity goes away completely. When only identities exist in the operator, then we simplify to an identity on a single wire to avoid getting rid of the operator completely.

If this is causing a problem, you can either directly create the Hamiltonian without simplification upon construction with qml.Hamiltonian(coeffs, ops, simplify=False) or use an operator arithmetic scalar product qml.sprod(2, qml.Identity((0,1,2))).

Was this causing problems in any specific use case?

To fix it, we would probably need to turn off automatic simplification.

isaacdevlugt commented 1 year ago

The problem where I was noticing this was similar to this:

n_qubits = 3

ham = 2.0 * qml.Identity(wires=range(n_qubits)) # just noticed that this happens without scalar mult as well
ham += 0.5 * qml.PauliX(0) @ qml.PauliX(1)

>>> ham.coeffs, ham.ops
(array([2. , 0.5]), [Identity(wires=[0]), PauliX(wires=[0]) @ PauliX(wires=[1])])

If I want to add a constant shift to a Hamiltonian, what's the best way? I feel like this should work.

albi3ro commented 1 year ago

The thing to remember is that we have implicit Identities everywhere nothing else exists.

You can double check this with the matrices:

>>> h1 = 2.0 * qml.Identity((0,1)) + qml.PauliX(0) @ qml.PauliX(1)
>>> h2 = qml.Hamiltonian([2.0, 1.0], [qml.Identity(0) @ qml.Identity(1), qml.PauliX(0) @ qml.PauliX(1)])
>>> qml.math.allclose(qml.matrix(h1), qml.matrix(h2))
True

So even though h1 has an identity simplified away during construction, it is exactly the same Hamiltonian as h2.

trbromley commented 1 year ago

we have implicit Identities everywhere nothing else exists

Should we do that? What was the motivation for this?

albi3ro commented 1 year ago

@trbromley What I mean by that is that qml.PauliX(0) @ qml.Identity(1) is basically the same thing as qml.PauliX(0).

Having to write out identities on every conceivable wire is going to be extremely inefficient and hard to understand.

Take for example qml.matrix(qml.PauliX(0), wire_order=[0,1]). That uses an implicit Identity on wire 1.

Without implicitly identities something like qml.PauliX(0) + qml.PauliY(1) would make no sense. What that sum actually means is qml.PauliX(0) @ qml.Identity(1) + qml.Identity(0) @ qml.PauliY(1).

isaacdevlugt commented 1 year ago

Hmmm, I see your points @albi3ro. There's a balance to strike with performance, other functionalities, and legibility and expected experience. I trust your call on this one! Feel free to close this issue 🙂