Parameter handling in SparsePauliOp (#8620)

* Parameter-friendly usage of Numpy methods

* remove pdb.set_trace()

* Updated target image in mpl backend visualization tests

* Fixed methods causing failing tests

* fixed lint

* Fixed formatting of variable in SparsePauliOP

* Fixed dtype casting (object->float, complex->float) tests

* Fixed failing tests

* Fixed lint

* Fixed import order for linter

* revert gate.py

* revert qdrift.py

* revert PauliSumOp

* update SparsePauliOp

* readable code (Jake's suggestion)

* add tests

* add docs

* fix docs

* fix typo

* add reno

* Update documentation

* use-parameters

* bind_parameters_to_one

Co-authored-by: Jesus Sistos <jesussistos.josb@gmail.com>
Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Ikko Hamamura 2022-09-29 08:59:48 +09:00 committed by GitHub
parent 368fbbb702
commit 2b7282d476
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 356 additions and 112 deletions

View File

@ -46,6 +46,31 @@ class SparsePauliOp(LinearOp):
using the :attr:`~SparsePauliOp.paulis` attribute. The coefficients
are stored as a complex Numpy array vector and can be accessed using
the :attr:`~SparsePauliOp.coeffs` attribute.
.. rubric:: Data type of coefficients
The default ``dtype`` of the internal ``coeffs`` Numpy array is ``complex128``. Users can
configure this by passing ``np.ndarray`` with a different dtype. For example, a parameterized
:class:`SparsePauliOp` can be made as follows:
.. code-block:: python
>>> import numpy as np
>>> from qiskit.circuit import ParameterVector
>>> from qiskit.quantum_info import SparsePauliOp
>>> SparsePauliOp(["II", "XZ"], np.array(ParameterVector("a", 2)))
SparsePauliOp(['II', 'XZ'],
coeffs=[ParameterExpression(1.0*a[0]), ParameterExpression(1.0*a[1])])
.. note::
Parameterized :class:`SparsePauliOp` does not support the following methods:
- ``to_matrix(sparse=True)`` since ``scipy.sparse`` cannot have objects as elements.
- ``to_operator()`` since :class:`~.quantum_info.Operator` does not support objects.
- ``sort``, ``argsort`` since :class:`.ParameterExpression` does not support comparison.
- ``equiv`` since :class:`.ParameterExpression`. cannot be converted into complex.
"""
def __init__(self, data, coeffs=None, *, ignore_pauli_phase=False, copy=True):
@ -86,10 +111,12 @@ class SparsePauliOp(LinearOp):
pauli_list = PauliList(data.copy() if copy and hasattr(data, "copy") else data)
dtype = coeffs.dtype if isinstance(coeffs, np.ndarray) else complex
if coeffs is None:
coeffs = np.ones(pauli_list.size, dtype=complex)
coeffs = np.ones(pauli_list.size, dtype=dtype)
else:
coeffs = np.array(coeffs, copy=copy, dtype=complex)
coeffs = np.array(coeffs, copy=copy, dtype=dtype)
if ignore_pauli_phase:
# Fast path used in copy operations, where the phase of the PauliList is already known
@ -101,7 +128,7 @@ class SparsePauliOp(LinearOp):
# move the phase of `pauli_list` to `self._coeffs`
phase = pauli_list._phase
count_y = pauli_list._count_y()
self._coeffs = np.asarray((-1j) ** (phase - count_y) * coeffs, dtype=complex)
self._coeffs = np.asarray((-1j) ** (phase - count_y) * coeffs, dtype=coeffs.dtype)
pauli_list._phase = np.mod(count_y, 4)
self._pauli_list = pauli_list
@ -132,9 +159,14 @@ class SparsePauliOp(LinearOp):
"""Entrywise comparison of two SparsePauliOp operators"""
return (
super().__eq__(other)
and self.coeffs.dtype == other.coeffs.dtype
and self.coeffs.shape == other.coeffs.shape
and np.allclose(self.coeffs, other.coeffs)
and self.paulis == other.paulis
and (
np.allclose(self.coeffs, other.coeffs)
if self.coeffs.dtype != object
else (self.coeffs == other.coeffs).all()
)
)
def equiv(self, other, atol: Optional[float] = None):
@ -385,7 +417,19 @@ class SparsePauliOp(LinearOp):
rtol = self.rtol
# Filter non-zero coefficients
non_zero = np.logical_not(np.isclose(self.coeffs, 0, atol=atol, rtol=rtol))
if self.coeffs.dtype == object:
def to_complex(coeff):
if not hasattr(coeff, "sympify"):
return coeff
sympified = coeff.sympify()
return complex(sympified) if sympified.is_Number else np.nan
non_zero = np.logical_not(
np.isclose([to_complex(x) for x in self.coeffs], 0, atol=atol, rtol=rtol)
)
else:
non_zero = np.logical_not(np.isclose(self.coeffs, 0, atol=atol, rtol=rtol))
paulis_x = self.paulis.x[non_zero]
paulis_z = self.paulis.z[non_zero]
nz_coeffs = self.coeffs[non_zero]
@ -398,16 +442,21 @@ class SparsePauliOp(LinearOp):
# No zero operator or duplicate operator
return self.copy()
coeffs = np.zeros(indexes.shape[0], dtype=complex)
coeffs = np.zeros(indexes.shape[0], dtype=self.coeffs.dtype)
np.add.at(coeffs, inverses, nz_coeffs)
# Delete zero coefficient rows
is_zero = np.isclose(coeffs, 0, atol=atol, rtol=rtol)
if self.coeffs.dtype == object:
is_zero = np.array(
[np.isclose(to_complex(coeff), 0, atol=atol, rtol=rtol) for coeff in coeffs]
)
else:
is_zero = np.isclose(coeffs, 0, atol=atol, rtol=rtol)
# Check edge case that we deleted all Paulis
# In this case we return an identity Pauli with a zero coefficient
if np.all(is_zero):
x = np.zeros((1, self.num_qubits), dtype=bool)
z = np.zeros((1, self.num_qubits), dtype=bool)
coeffs = np.array([0j], dtype=complex)
coeffs = np.array([0j], dtype=self.coeffs.dtype)
else:
non_zero = np.logical_not(is_zero)
non_zero_indexes = indexes[non_zero]
@ -659,7 +708,7 @@ class SparsePauliOp(LinearOp):
return SparsePauliOp(paulis, coeffs, copy=False)
@staticmethod
def from_list(obj):
def from_list(obj, dtype=complex):
"""Construct from a list of Pauli strings and coefficients.
For example, the 5-qubit Hamiltonian
@ -677,6 +726,7 @@ class SparsePauliOp(LinearOp):
Args:
obj (Iterable[Tuple[str, complex]]): The list of 2-tuples specifying the Pauli terms.
dtype (type): The dtype of coeffs (Default complex).
Returns:
SparsePauliOp: The SparsePauliOp representation of the Pauli terms.
@ -693,7 +743,7 @@ class SparsePauliOp(LinearOp):
# determine the number of qubits
num_qubits = len(obj[0][0])
coeffs = np.zeros(size, dtype=complex)
coeffs = np.zeros(size, dtype=dtype)
labels = np.zeros(size, dtype=f"<U{num_qubits}")
for i, item in enumerate(obj):
labels[i] = item[0]
@ -703,7 +753,7 @@ class SparsePauliOp(LinearOp):
return SparsePauliOp(paulis, coeffs, copy=False)
@staticmethod
def from_sparse_list(obj, num_qubits, do_checks=True):
def from_sparse_list(obj, num_qubits, do_checks=True, dtype=complex):
"""Construct from a list of local Pauli strings and coefficients.
Each list element is a 3-tuple of a local Pauli string, indices where to apply it,
@ -729,6 +779,7 @@ class SparsePauliOp(LinearOp):
obj (Iterable[Tuple[str, List[int], complex]]): The list 3-tuples specifying the Paulis.
num_qubits (int): The number of qubits of the operator.
do_checks (bool): The flag of checking if the input indices are not duplicated.
dtype (type): The dtype of coeffs (Default complex).
Returns:
SparsePauliOp: The SparsePauliOp representation of the Pauli terms.
@ -744,7 +795,7 @@ class SparsePauliOp(LinearOp):
if size == 0:
raise QiskitError("Input Pauli list is empty.")
coeffs = np.zeros(size, dtype=complex)
coeffs = np.zeros(size, dtype=dtype)
labels = np.zeros(size, dtype=f"<U{num_qubits}")
for i, (paulis, indices, coeff) in enumerate(obj):
@ -781,7 +832,9 @@ class SparsePauliOp(LinearOp):
"""
# Dtype for a structured array with string labels and complex coeffs
pauli_labels = self.paulis.to_labels(array=True)
labels = np.zeros(self.size, dtype=[("labels", pauli_labels.dtype), ("coeffs", "c16")])
labels = np.zeros(
self.size, dtype=[("labels", pauli_labels.dtype), ("coeffs", self.coeffs.dtype)]
)
labels["labels"] = pauli_labels
labels["coeffs"] = self.coeffs
if array:

View File

@ -0,0 +1,25 @@
---
features:
- |
:class:`.SparsePauliOp`\ s can now be constructed with coefficient arrays
that are general Python objects. This is purely intended for use with Terra's
:class:`.ParameterExpression` objects; other objects may work, but do not
have first-class support. Some :class:`.SparsePauliOp` methods (such as
conversion to other class representations) may not work when using ``object``
arrays, if the desired target cannot represent these general arrays.
For example, a :class:`.ParameterExpression` :class:`.SparsePauliOp` could
be constructed by::
import numpy as np
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
print(SparsePauliOp(["II", "XZ"], np.array([Parameter("a"), Parameter("b")])))
which gives
.. code-block:: text
SparsePauliOp(['II', 'XZ'],
coeffs=[ParameterExpression(1.0*a), ParameterExpression(1.0*b)])

View File

@ -20,6 +20,7 @@ import numpy as np
from ddt import ddt
from qiskit import QiskitError
from qiskit.circuit import Parameter, ParameterVector
from qiskit.quantum_info.operators import Operator, Pauli, PauliList, PauliTable, SparsePauliOp
from qiskit.test import QiskitTestCase
@ -83,6 +84,18 @@ class TestSparsePauliOpInit(QiskitTestCase):
np.testing.assert_array_equal(spp_op.coeffs, [1, 2, 3, -4, 5j, -6j])
paulis.phase = 0
self.assertEqual(spp_op.paulis, paulis)
paulis = PauliList(labels)
with self.subTest(msg="with Parameterized coeffs"):
params = ParameterVector("params", 6)
coeffs = np.array(params)
spp_op = SparsePauliOp(paulis, coeffs)
target = coeffs.copy()
target[3] *= -1
target[4] *= 1j
target[5] *= -1j
np.testing.assert_array_equal(spp_op.coeffs, target)
paulis.phase = 0
self.assertEqual(spp_op.paulis, paulis)
def test_sparse_pauli_op_init(self):
"""Test SparsePauliOp initialization."""
@ -156,6 +169,14 @@ class TestSparsePauliOpConversions(QiskitTestCase):
np.testing.assert_array_equal(spp_op.coeffs, coeffs)
self.assertEqual(spp_op.paulis, PauliList(labels))
def test_from_list_parameters(self):
"""Test from_list method with parameters."""
labels = ["XXZ", "IXI", "YZZ", "III"]
coeffs = ParameterVector("a", 4)
spp_op = SparsePauliOp.from_list(zip(labels, coeffs), dtype=object)
np.testing.assert_array_equal(spp_op.coeffs, coeffs)
self.assertEqual(spp_op.paulis, PauliList(labels))
def test_from_index_list(self):
"""Test from_list method specifying the Paulis via indices."""
expected_labels = ["XXZ", "IXI", "YIZ", "III"]
@ -166,6 +187,18 @@ class TestSparsePauliOpConversions(QiskitTestCase):
np.testing.assert_array_equal(spp_op.coeffs, coeffs)
self.assertEqual(spp_op.paulis, PauliList(expected_labels))
def test_from_index_list_parameters(self):
"""Test from_list method specifying the Paulis via indices with paramteres."""
expected_labels = ["XXZ", "IXI", "YIZ", "III"]
paulis = ["XXZ", "X", "YZ", ""]
indices = [[2, 1, 0], [1], [2, 0], []]
coeffs = ParameterVector("a", 4)
spp_op = SparsePauliOp.from_sparse_list(
zip(paulis, indices, coeffs), num_qubits=3, dtype=object
)
np.testing.assert_array_equal(spp_op.coeffs, coeffs)
self.assertEqual(spp_op.paulis, PauliList(expected_labels))
def test_from_index_list_endianness(self):
"""Test the construction from index list has the right endianness."""
spp_op = SparsePauliOp.from_sparse_list([("ZX", [1, 4], 1)], num_qubits=5)
@ -204,6 +237,16 @@ class TestSparsePauliOpConversions(QiskitTestCase):
target += coeff * pauli_mat(label)
np.testing.assert_array_equal(spp_op.to_matrix(), target)
def to_matrix_parameters(self):
"""Test to_matrix method for parameterized SparsePauliOp."""
labels = ["XI", "YZ", "YY", "ZZ"]
coeffs = ParameterVector("a", 4)
spp_op = SparsePauliOp(labels, coeffs)
target = np.zeros((4, 4), dtype=object)
for coeff, label in zip(coeffs, labels):
target += coeff * pauli_mat(label)
np.testing.assert_array_equal(spp_op.to_matrix(), target)
def to_operator(self):
"""Test to_operator method."""
labels = ["XI", "YZ", "YY", "ZZ"]
@ -222,6 +265,14 @@ class TestSparsePauliOpConversions(QiskitTestCase):
target = list(zip(labels, coeffs))
self.assertEqual(op.to_list(), target)
def to_list_parameters(self):
"""Test to_operator method with paramters."""
labels = ["XI", "YZ", "YY", "ZZ"]
coeffs = np.array(ParameterVector("a", 4))
op = SparsePauliOp(labels, coeffs)
target = list(zip(labels, coeffs))
self.assertEqual(op.to_list(), target)
class TestSparsePauliOpIteration(QiskitTestCase):
"""Tests for SparsePauliOp iterators class."""
@ -234,6 +285,14 @@ class TestSparsePauliOpIteration(QiskitTestCase):
for idx, i in enumerate(op):
self.assertEqual(i, SparsePauliOp(labels[idx], coeffs[[idx]]))
def test_enumerate_parameters(self):
"""Test enumerate with SparsePauliOp with parameters."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
coeffs = np.array(ParameterVector("a", 6))
op = SparsePauliOp(labels, coeffs)
for idx, i in enumerate(op):
self.assertEqual(i, SparsePauliOp(labels[idx], coeffs[[idx]]))
def test_iter(self):
"""Test iter with SparsePauliOp."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
@ -242,6 +301,14 @@ class TestSparsePauliOpIteration(QiskitTestCase):
for idx, i in enumerate(iter(op)):
self.assertEqual(i, SparsePauliOp(labels[idx], coeffs[[idx]]))
def test_iter_parameters(self):
"""Test iter with SparsePauliOp with parameters."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
coeffs = np.array(ParameterVector("a", 6))
op = SparsePauliOp(labels, coeffs)
for idx, i in enumerate(iter(op)):
self.assertEqual(i, SparsePauliOp(labels[idx], coeffs[[idx]]))
def test_label_iter(self):
"""Test SparsePauliOp label_iter method."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
@ -250,6 +317,14 @@ class TestSparsePauliOpIteration(QiskitTestCase):
for idx, i in enumerate(op.label_iter()):
self.assertEqual(i, (labels[idx], coeffs[idx]))
def test_label_iter_parameters(self):
"""Test SparsePauliOp label_iter method with parameters."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
coeffs = np.array(ParameterVector("a", 6))
op = SparsePauliOp(labels, coeffs)
for idx, i in enumerate(op.label_iter()):
self.assertEqual(i, (labels[idx], coeffs[idx]))
def test_matrix_iter(self):
"""Test SparsePauliOp dense matrix_iter method."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
@ -258,6 +333,14 @@ class TestSparsePauliOpIteration(QiskitTestCase):
for idx, i in enumerate(op.matrix_iter()):
np.testing.assert_array_equal(i, coeffs[idx] * pauli_mat(labels[idx]))
def test_matrix_iter_parameters(self):
"""Test SparsePauliOp dense matrix_iter method. with parameters"""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
coeffs = np.array(ParameterVector("a", 6))
op = SparsePauliOp(labels, coeffs)
for idx, i in enumerate(op.matrix_iter()):
np.testing.assert_array_equal(i, coeffs[idx] * pauli_mat(labels[idx]))
def test_matrix_iter_sparse(self):
"""Test SparsePauliOp sparse matrix_iter method."""
labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"]
@ -267,85 +350,116 @@ class TestSparsePauliOpIteration(QiskitTestCase):
np.testing.assert_array_equal(i.toarray(), coeffs[idx] * pauli_mat(labels[idx]))
def bind_parameters_to_one(array):
"""Bind parameters to one. The purpose of using this method is to bind some value and
use ``assert_allclose``, since it is impossible to verify equivalence in the case of
numerical errors with parameters existing.
"""
def bind_one(a):
parameters = a.parameters
return complex(a.bind(dict(zip(parameters, [1] * len(parameters)))))
return np.vectorize(bind_one, otypes=[complex])(array)
@ddt
class TestSparsePauliOpMethods(QiskitTestCase):
"""Tests for SparsePauliOp operator methods."""
RNG = np.random.default_rng(1994)
def random_spp_op(self, num_qubits, num_terms):
def setUp(self):
super().setUp()
self.parameter_names = (f"param_{x}" for x in it.count())
def random_spp_op(self, num_qubits, num_terms, use_parameters=False):
"""Generate a pseudo-random SparsePauliOp"""
coeffs = self.RNG.uniform(-1, 1, size=num_terms) + 1j * self.RNG.uniform(
-1, 1, size=num_terms
)
if use_parameters:
coeffs = np.array(ParameterVector(next(self.parameter_names), num_terms))
else:
coeffs = self.RNG.uniform(-1, 1, size=num_terms) + 1j * self.RNG.uniform(
-1, 1, size=num_terms
)
labels = [
"".join(self.RNG.choice(["I", "X", "Y", "Z"], size=num_qubits))
for _ in range(num_terms)
]
return SparsePauliOp(labels, coeffs)
@combine(num_qubits=[1, 2, 3, 4])
def test_conjugate(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_conjugate(self, num_qubits, use_parameters):
"""Test conjugate method for {num_qubits}-qubits."""
spp_op = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op).conjugate()
spp_op = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op.to_matrix().conjugate()
op = spp_op.conjugate()
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
np.testing.assert_array_equal(value, target)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3, 4])
def test_transpose(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_transpose(self, num_qubits, use_parameters):
"""Test transpose method for {num_qubits}-qubits."""
spp_op = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op).transpose()
spp_op = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op.to_matrix().transpose()
op = spp_op.transpose()
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
np.testing.assert_array_equal(value, target)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3, 4])
def test_adjoint(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_adjoint(self, num_qubits, use_parameters):
"""Test adjoint method for {num_qubits}-qubits."""
spp_op = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op).adjoint()
spp_op = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op.to_matrix().transpose().conjugate()
op = spp_op.adjoint()
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
np.testing.assert_array_equal(value, target)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3, 4])
def test_compose(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_compose(self, num_qubits, use_parameters):
"""Test {num_qubits}-qubit compose methods."""
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op1).compose(Operator(spp_op2))
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op2.to_matrix() @ spp_op1.to_matrix()
op = spp_op1.compose(spp_op2)
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
op = spp_op1 & spp_op2
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3, 4])
def test_dot(self, num_qubits):
"""Test {num_qubits}-qubit compose methods."""
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op1).dot(Operator(spp_op2))
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_dot(self, num_qubits, use_parameters):
"""Test {num_qubits}-qubit dot methods."""
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op1.to_matrix() @ spp_op2.to_matrix()
op = spp_op1.dot(spp_op2)
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
op = spp_op1 @ spp_op2
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3])
@ -379,48 +493,60 @@ class TestSparsePauliOpMethods(QiskitTestCase):
self.assertEqual(value, target)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits1=[1, 2, 3], num_qubits2=[1, 2, 3])
def test_tensor(self, num_qubits1, num_qubits2):
@combine(num_qubits1=[1, 2, 3], num_qubits2=[1, 2, 3], use_parameters=[True, False])
def test_tensor(self, num_qubits1, num_qubits2, use_parameters):
"""Test tensor method for {num_qubits1} and {num_qubits2} qubits."""
spp_op1 = self.random_spp_op(num_qubits1, 2**num_qubits1)
spp_op2 = self.random_spp_op(num_qubits2, 2**num_qubits2)
target = Operator(spp_op1).tensor(Operator(spp_op2))
spp_op1 = self.random_spp_op(num_qubits1, 2**num_qubits1, use_parameters)
spp_op2 = self.random_spp_op(num_qubits2, 2**num_qubits2, use_parameters)
target = np.kron(spp_op1.to_matrix(), spp_op2.to_matrix())
op = spp_op1.tensor(spp_op2)
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits1=[1, 2, 3], num_qubits2=[1, 2, 3])
def test_expand(self, num_qubits1, num_qubits2):
@combine(num_qubits1=[1, 2, 3], num_qubits2=[1, 2, 3], use_parameters=[True, False])
def test_expand(self, num_qubits1, num_qubits2, use_parameters):
"""Test expand method for {num_qubits1} and {num_qubits2} qubits."""
spp_op1 = self.random_spp_op(num_qubits1, 2**num_qubits1)
spp_op2 = self.random_spp_op(num_qubits2, 2**num_qubits2)
target = Operator(spp_op1).expand(Operator(spp_op2))
spp_op1 = self.random_spp_op(num_qubits1, 2**num_qubits1, use_parameters)
spp_op2 = self.random_spp_op(num_qubits2, 2**num_qubits2, use_parameters)
target = np.kron(spp_op2.to_matrix(), spp_op1.to_matrix())
op = spp_op1.expand(spp_op2)
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3, 4])
def test_add(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_add(self, num_qubits, use_parameters):
"""Test + method for {num_qubits} qubits."""
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op1) + Operator(spp_op2)
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op1.to_matrix() + spp_op2.to_matrix()
op = spp_op1 + spp_op2
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3, 4])
def test_sub(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_sub(self, num_qubits, use_parameters):
"""Test + method for {num_qubits} qubits."""
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op1) - Operator(spp_op2)
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
target = spp_op1.to_matrix() - spp_op2.to_matrix()
op = spp_op1 - spp_op2
value = op.to_operator()
self.assertEqual(value, target)
value = op.to_matrix()
if use_parameters:
value = bind_parameters_to_one(value)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3])
@ -447,29 +573,44 @@ class TestSparsePauliOpMethods(QiskitTestCase):
self.assertEqual(value, target)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3], value=[0, 1, 1j, -3 + 4.4j, np.int64(2)])
def test_mul(self, num_qubits, value):
@combine(num_qubits=[1, 2, 3], value=[0, 1, 1j, -3 + 4.4j, np.int64(2)], param=[None, "a"])
def test_mul(self, num_qubits, value, param):
"""Test * method for {num_qubits} qubits and value {value}."""
spp_op = self.random_spp_op(num_qubits, 2**num_qubits)
target = value * Operator(spp_op)
spp_op = self.random_spp_op(num_qubits, 2**num_qubits, param)
target = value * spp_op.to_matrix()
op = value * spp_op
value_mat = op.to_operator()
self.assertEqual(value_mat, target)
value_mat = op.to_matrix()
if value != 0 and param is not None:
value_mat = bind_parameters_to_one(value_mat)
target = bind_parameters_to_one(target)
if value == 0:
np.testing.assert_array_equal(value_mat, target.astype(complex))
else:
np.testing.assert_allclose(value_mat, target)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
target = Operator(spp_op) * value
target = spp_op.to_matrix() * value
op = spp_op * value
value_mat = op.to_operator()
self.assertEqual(value_mat, target)
value_mat = op.to_matrix()
if value != 0 and param is not None:
value_mat = bind_parameters_to_one(value_mat)
target = bind_parameters_to_one(target)
if value == 0:
np.testing.assert_array_equal(value_mat, target.astype(complex))
else:
np.testing.assert_allclose(value_mat, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
@combine(num_qubits=[1, 2, 3], value=[1, 1j, -3 + 4.4j])
def test_div(self, num_qubits, value):
@combine(num_qubits=[1, 2, 3], value=[1, 1j, -3 + 4.4j], param=[None, "a"])
def test_div(self, num_qubits, value, param):
"""Test / method for {num_qubits} qubits and value {value}."""
spp_op = self.random_spp_op(num_qubits, 2**num_qubits)
target = Operator(spp_op) / value
spp_op = self.random_spp_op(num_qubits, 2**num_qubits, param)
target = spp_op.to_matrix() / value
op = spp_op / value
value = op.to_operator()
self.assertEqual(value, target)
value_mat = op.to_matrix()
if param is not None:
value_mat = bind_parameters_to_one(value_mat)
target = bind_parameters_to_one(target)
np.testing.assert_allclose(value_mat, target, atol=1e-8)
np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size))
def test_simplify(self):
@ -510,6 +651,19 @@ class TestSparsePauliOpMethods(QiskitTestCase):
np.testing.assert_array_equal(zero_op.paulis.phase, np.zeros(zero_op.size))
np.testing.assert_array_equal(simplified_op.paulis.phase, np.zeros(simplified_op.size))
def test_simplify_parameters(self):
"""Test simplify methods for parameterized SparsePauliOp."""
a = Parameter("a")
coeffs = np.array([a, -a, 0, a, a, a, 2 * a])
labels = ["IXI", "IXI", "ZZZ", "III", "III", "XXX", "XXX"]
spp_op = SparsePauliOp(labels, coeffs)
simplified_op = spp_op.simplify()
target_coeffs = np.array([2 * a, 3 * a])
target_labels = ["III", "XXX"]
target_op = SparsePauliOp(target_labels, target_coeffs)
self.assertEqual(simplified_op, target_op)
np.testing.assert_array_equal(simplified_op.paulis.phase, np.zeros(simplified_op.size))
def test_sort(self):
"""Test sort method."""
with self.assertRaises(QiskitError):
@ -656,14 +810,22 @@ class TestSparsePauliOpMethods(QiskitTestCase):
expected = SparsePauliOp(["I"], coeffs=[0.0])
self.assertEqual(simplified, expected)
@combine(num_qubits=[1, 2, 3, 4], num_ops=[1, 2, 3, 4])
def test_sum(self, num_qubits, num_ops):
@combine(num_qubits=[1, 2, 3, 4], num_ops=[1, 2, 3, 4], param=[None, "a"])
def test_sum(self, num_qubits, num_ops, param):
"""Test sum method for {num_qubits} qubits with {num_ops} operators."""
ops = [self.random_spp_op(num_qubits, 2**num_qubits) for _ in range(num_ops)]
ops = [
self.random_spp_op(
num_qubits, 2**num_qubits, param if param is None else f"{param}_{i}"
)
for i in range(num_ops)
]
sum_op = SparsePauliOp.sum(ops)
value = Operator(sum_op)
target_operator = sum((Operator(op) for op in ops[1:]), Operator(ops[0]))
self.assertEqual(value, target_operator)
value = sum_op.to_matrix()
target_operator = sum((op.to_matrix() for op in ops[1:]), ops[0].to_matrix())
if param is not None:
value = bind_parameters_to_one(value)
target_operator = bind_parameters_to_one(target_operator)
np.testing.assert_allclose(value, target_operator, atol=1e-8)
target_spp_op = sum((op for op in ops[1:]), ops[0])
self.assertEqual(sum_op, target_spp_op)
np.testing.assert_array_equal(sum_op.paulis.phase, np.zeros(sum_op.size))
@ -678,12 +840,12 @@ class TestSparsePauliOpMethods(QiskitTestCase):
with self.assertRaises(QiskitError):
SparsePauliOp.sum([1, 2])
@combine(num_qubits=[1, 2, 3, 4])
def test_eq(self, num_qubits):
@combine(num_qubits=[1, 2, 3, 4], use_parameters=[True, False])
def test_eq(self, num_qubits, use_parameters):
"""Test __eq__ method for {num_qubits} qubits."""
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op3 = self.random_spp_op(num_qubits, 2**num_qubits)
spp_op1 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
spp_op2 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
spp_op3 = self.random_spp_op(num_qubits, 2**num_qubits, use_parameters)
zero = spp_op3 - spp_op3
self.assertEqual(spp_op1, spp_op1)
self.assertEqual(spp_op2, spp_op2)
@ -739,7 +901,8 @@ class TestSparsePauliOpMethods(QiskitTestCase):
self.assertNotEqual(spp_op1, spp_op2)
self.assertTrue(spp_op1.equiv(spp_op2))
def test_group_commuting(self):
@combine(parameterized=[True, False])
def test_group_commuting(self, parameterized):
"""Test general grouping commuting operators"""
def commutes(left: Pauli, right: Pauli) -> bool:
@ -747,8 +910,11 @@ class TestSparsePauliOpMethods(QiskitTestCase):
input_labels = ["IX", "IY", "IZ", "XX", "YY", "ZZ", "XY", "YX", "ZX", "ZY", "XZ", "YZ"]
np.random.shuffle(input_labels)
coefs = np.random.random(len(input_labels)) + np.random.random(len(input_labels)) * 1j
sparse_pauli_list = SparsePauliOp(input_labels, coefs)
if parameterized:
coeffs = np.array(ParameterVector("a", len(input_labels)))
else:
coeffs = np.random.random(len(input_labels)) + np.random.random(len(input_labels)) * 1j
sparse_pauli_list = SparsePauliOp(input_labels, coeffs)
groups = sparse_pauli_list.group_commuting()
# checking that every input Pauli in sparse_pauli_list is in a group in the ouput
output_labels = [pauli.to_label() for group in groups for pauli in group.paulis]
@ -757,7 +923,7 @@ class TestSparsePauliOpMethods(QiskitTestCase):
paulis_coeff_dict = dict(
sum([list(zip(group.paulis.to_labels(), group.coeffs)) for group in groups], [])
)
self.assertDictEqual(dict(zip(input_labels, coefs)), paulis_coeff_dict)
self.assertDictEqual(dict(zip(input_labels, coeffs)), paulis_coeff_dict)
# Within each group, every operator commutes with every other operator.
for group in groups: