Skip to content

Commit

Permalink
Allow non-hermitian observables (#2960)
Browse files Browse the repository at this point in the history
* refactor: ♻️ Raise warning instead of error when using non-hermitian observables.

* feat: ✨ Allow sampling with symbolic operators.

* feat: ✨ Add qml.is_hermitian and qml.is_unitary checks.

* refactor (is_hermitian): ♻️ Avoid slow computation.

* fix (Adjoint): 🐛 Use getattr.

* fix: 🐛 Fix small bug.

* test: 🧪 Add tests.

* revert: ⏪ Use eigh always.

* revert (Prod): ⏪ Revert changes.

* fix: 🐛 Fix small bug.

* fix tests

* fix tests

* Update tests/ops/functions/test_is_hermitian.py

Co-authored-by: Christina Lee <christina@xanadu.ai>

* refactor: ♻️ Change warning message.

* Update pennylane/ops/functions/is_unitary.py

Co-authored-by: Christina Lee <christina@xanadu.ai>

* docs: 📝 Improve docstrings.

* docs: 📝 Improve docstrings.

* test: 🧪 Fix tests.

* style: 🎨 Fix codefactor errors.

* Update pennylane/ops/functions/is_unitary.py

Co-authored-by: Christina Lee <christina@xanadu.ai>

* chore (changelog): ✏️ Add feature to changelog.

* feat (MultiControlledX): ✨ Set is_hermitian to True.

* black

* docs: 📝 Add docs.

* chore (changelog): ✏️ Address comments.

* Update pennylane/ops/functions/is_hermitian.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update pennylane/ops/functions/is_hermitian.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update pennylane/ops/functions/is_unitary.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update tests/ops/op_math/test_prod.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update tests/ops/op_math/test_sum.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update tests/ops/op_math/test_sum.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update tests/ops/op_math/test_sprod.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Update tests/ops/op_math/test_sprod.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* Address comments.

* remove unused import.

* address comments

* small change

* small change

* add tests

* use hadamard in tests.

* fix test

* Update tests/ops/op_math/test_prod.py

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>

* small changes.

* small changes.

* add tests

* change xfail tests

Co-authored-by: Christina Lee <christina@xanadu.ai>
Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>
  • Loading branch information
3 people committed Aug 22, 2022
1 parent c5213dc commit 754f2c9
Show file tree
Hide file tree
Showing 15 changed files with 462 additions and 102 deletions.
2 changes: 2 additions & 0 deletions doc/introduction/operations.rst
Expand Up @@ -71,6 +71,8 @@ Operator to Other functions

~pennylane.matrix
~pennylane.eigvals
~pennylane.is_hermitian
~pennylane.is_unitary

These operator functions act on operators and return other data types.
All operator functions can be used on instantiated operators.
Expand Down
24 changes: 21 additions & 3 deletions doc/releases/changelog-dev.md
Expand Up @@ -61,9 +61,9 @@
will return the randomized Pauli measurements (the `recipes`) that are performed
for each qubit, identified as a unique integer:

- 0 for Pauli X
- 1 for Pauli Y
- 2 for Pauli Z
* 0 for Pauli X
* 1 for Pauli Y
* 2 for Pauli Z

It also returns the measurement results (the `bits`), which is `0` if the 1 eigenvalue
is sampled, and `1` if the -1 eigenvalue is sampled.
Expand All @@ -79,6 +79,7 @@
qml.CNOT(wires=[0, 1])
return qml.classical_shadow(wires=[0, 1])
```

```pycon
>>> bits, recipes = circuit()
tensor([[0, 0],
Expand Down Expand Up @@ -112,8 +113,24 @@
RX(-1, wires=[0])
```

* Added the `qml.is_hermitian` and `qml.is_unitary` function checks.
[(#2960)](https://github.com/PennyLaneAI/pennylane/pull/2960)

```pycon
>>> op = qml.PauliX(wires=0)
>>> qml.is_hermitian(op)
True
>>> op2 = qml.RX(0.54, wires=0)
>>> qml.is_hermitian(op2)
False
```

<h3>Breaking changes</h3>

* Measuring an operator that might not be hermitian as an observable now raises a warning instead of an
error. To definitively determine whether or not an operator is hermitian, use `qml.is_hermitian`.
[(#2960)](https://github.com/PennyLaneAI/pennylane/pull/2960)

<h3>Deprecations</h3>

<h3>Documentation</h3>
Expand All @@ -131,6 +148,7 @@ Josh Izaac,
Edward Jiang,
Ankit Khandelwal,
Korbinian Kottmann,
Albert Mitjans Coma,
Rashid N H M,
Zeyue Niu,
Mudit Pandey,
Expand Down
22 changes: 5 additions & 17 deletions pennylane/measurements.py
Expand Up @@ -21,6 +21,7 @@
import copy
import functools
import uuid
import warnings
from enum import Enum
from typing import Generic, TypeVar

Expand Down Expand Up @@ -596,9 +597,7 @@ def circuit(x):
QuantumFunctionError: `op` is not an instance of :class:`~.Observable`
"""
if not op.is_hermitian:
raise qml.QuantumFunctionError(
f"{op.name} is not an observable: cannot be used with expval"
)
warnings.warn(f"{op.name} might not be hermitian.")

return MeasurementProcess(Expectation, obs=op)

Expand Down Expand Up @@ -631,8 +630,7 @@ def circuit(x):
QuantumFunctionError: `op` is not an instance of :class:`~.Observable`
"""
if not op.is_hermitian:
raise qml.QuantumFunctionError(f"{op.name} is not an observable: cannot be used with var")

warnings.warn(f"{op.name} might not be hermitian.")
return MeasurementProcess(Variance, obs=op)


Expand Down Expand Up @@ -710,12 +708,7 @@ def circuit(x):
observable ``obs``.
"""
if op is not None and not op.is_hermitian: # None type is also allowed for op
raise qml.QuantumFunctionError(
f"{op.name} is not an observable: cannot be used with sample"
)

if isinstance(op, (qml.ops.Sum, qml.ops.SProd, qml.ops.Prod)): # pylint: disable=no-member
raise qml.QuantumFunctionError("Symbolic Operations are not supported for sampling yet.")
warnings.warn(f"{op.name} might not be hermitian.")

if wires is not None:
if op is not None:
Expand Down Expand Up @@ -799,12 +792,7 @@ def circuit(x):
observable ``obs``.
"""
if op is not None and not op.is_hermitian: # None type is also allowed for op
raise qml.QuantumFunctionError(
f"{op.name} is not an observable: cannot be used with counts"
)

if isinstance(op, (qml.ops.Sum, qml.ops.SProd, qml.ops.Prod)): # pylint: disable=no-member
raise qml.QuantumFunctionError("Symbolic Operations are not supported for sampling yet.")
warnings.warn(f"{op.name} might not be hermitian.")

if wires is not None:
if op is not None:
Expand Down
2 changes: 2 additions & 0 deletions pennylane/ops/functions/__init__.py
Expand Up @@ -28,5 +28,7 @@
from .eigvals import eigvals
from .equal import equal
from .generator import generator
from .is_hermitian import is_hermitian
from .is_unitary import is_unitary
from .matrix import matrix
from .simplify import simplify
48 changes: 48 additions & 0 deletions pennylane/ops/functions/is_hermitian.py
@@ -0,0 +1,48 @@
# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the qml.is_hermitian function.
"""
import pennylane as qml
from pennylane.operation import Operator


def is_hermitian(op: Operator):
"""Check if the operation is hermitian.
A hermitian matrix is a complex square matrix that is equal to its own adjoint
.. math:: O† = O
Args:
op (~.operation.Operator): the operator to check against
Returns:
bool: True if the operation is hermitian, False otherwise
.. note::
This check might be expensive for large operators.
**Example**
>>> op = qml.PauliX(wires=0)
>>> qml.is_hermitian(op)
True
>>> op2 = qml.RX(0.54, wires=0)
>>> qml.is_hermitian(op2)
False
"""
if op.is_hermitian is True:
return True
return qml.math.allclose(qml.matrix(op), qml.matrix(qml.adjoint(op)))
49 changes: 49 additions & 0 deletions pennylane/ops/functions/is_unitary.py
@@ -0,0 +1,49 @@
# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the qml.is_unitary function.
"""
import pennylane as qml
from pennylane.operation import Operator


def is_unitary(op: Operator):
"""Check if the operation is unitary.
A matrix is unitary if its adjoint is also its inverse, that is, if
.. math:: O†O = OO† = I
Args:
op (~.operation.Operator): the operator to check against
Returns:
bool: True if the operation is unitary, False otherwise
.. note::
This check might be expensive for large operators.
**Example**
>>> op = qml.RX(0.54, wires=0)
>>> qml.is_unitary(op)
True
>>> op2 = op + op
>>> qml.is_unitary(op2)
False
"""
identity_mat = qml.math.eye(2 ** len(op.wires))
adj_op = qml.adjoint(op)
op_prod_adjoint_matrix = qml.matrix(qml.prod(op, adj_op))
return qml.math.allclose(op_prod_adjoint_matrix, identity_mat)
2 changes: 1 addition & 1 deletion pennylane/ops/op_math/adjoint_class.py
Expand Up @@ -210,7 +210,7 @@ def has_matrix(self):
return self.base.has_matrix if self.base.batch_size is None else False

def matrix(self, wire_order=None):
if self.base.batch_size is not None:
if getattr(self.base, "batch_size", None) is not None:
raise qml.operation.MatrixUndefinedError

if isinstance(self.base, qml.Hamiltonian):
Expand Down
31 changes: 30 additions & 1 deletion pennylane/ops/qubit/non_parametric_ops.py
Expand Up @@ -17,8 +17,9 @@
"""
# pylint:disable=abstract-method,arguments-differ,protected-access,invalid-overridden-method, no-member
import cmath
from copy import copy
import warnings
from copy import copy

import numpy as np
from scipy.linalg import block_diag

Expand Down Expand Up @@ -991,6 +992,10 @@ def _controlled(self, wire):
def control_wires(self):
return Wires(self.wires[0])

@property
def is_hermitian(self):
return True


class CZ(Operation):
r"""CZ(wires)
Expand Down Expand Up @@ -1080,6 +1085,10 @@ def pow(self, z):
def control_wires(self):
return Wires(self.wires[0])

@property
def is_hermitian(self):
return True


class CY(Operation):
r"""CY(wires)
Expand Down Expand Up @@ -1175,6 +1184,10 @@ def pow(self, z):
def control_wires(self):
return Wires(self.wires[0])

@property
def is_hermitian(self):
return True


class SWAP(Operation):
r"""SWAP(wires)
Expand Down Expand Up @@ -1258,6 +1271,10 @@ def adjoint(self):
def _controlled(self, wire):
CSWAP(wires=wire + self.wires)

@property
def is_hermitian(self):
return True


class ECR(Operation):
r""" ECR(wires)
Expand Down Expand Up @@ -1745,6 +1762,10 @@ def adjoint(self):
def control_wires(self):
return Wires(self.wires[0])

@property
def is_hermitian(self):
return True


class Toffoli(Operation):
r"""Toffoli(wires)
Expand Down Expand Up @@ -1883,6 +1904,10 @@ def pow(self, z):
def control_wires(self):
return Wires(self.wires[:2])

@property
def is_hermitian(self):
return True


class MultiControlledX(Operation):
r"""MultiControlledX(control_wires, wires, control_values)
Expand Down Expand Up @@ -2196,6 +2221,10 @@ def _decomposition_with_one_worker(control_wires, target_wire, work_wire):

return gates

@property
def is_hermitian(self):
return True


class Barrier(Operation):
r"""Barrier(wires)
Expand Down

0 comments on commit 754f2c9

Please sign in to comment.