mirror of https://github.com/Qiskit/qiskit.git
360 lines
14 KiB
Python
360 lines
14 KiB
Python
# This code is part of Qiskit.
|
|
#
|
|
# (C) Copyright IBM 2024.
|
|
#
|
|
# This code is licensed under the Apache License, Version 2.0. You may
|
|
# obtain a copy of this license in the LICENSE.txt file in the root directory
|
|
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
|
#
|
|
# Any modifications or derivative works of this code must retain this
|
|
# copyright notice, and modified files need to carry a notice indicating
|
|
# that they have been altered from the originals.
|
|
|
|
"""Test ObservablesArray"""
|
|
|
|
import itertools as it
|
|
import ddt
|
|
import numpy as np
|
|
|
|
import qiskit.quantum_info as qi
|
|
from qiskit.primitives.containers.observables_array import ObservablesArray
|
|
from test import QiskitTestCase # pylint: disable=wrong-import-order
|
|
|
|
|
|
@ddt.ddt
|
|
class ObservablesArrayTestCase(QiskitTestCase):
|
|
"""Test the ObservablesArray class"""
|
|
|
|
@ddt.data(0, 1, 2)
|
|
def test_coerce_observable_str(self, num_qubits):
|
|
"""Test coerce_observable for allowed basis str input"""
|
|
for chars in it.permutations(ObservablesArray.ALLOWED_BASIS, num_qubits):
|
|
label = "".join(chars)
|
|
obs = ObservablesArray.coerce_observable(label)
|
|
self.assertEqual(obs, {label: 1})
|
|
|
|
def test_coerce_observable_custom_basis(self):
|
|
"""Test coerce_observable for custom al flowed basis"""
|
|
|
|
class PauliArray(ObservablesArray):
|
|
"""Custom array allowing only Paulis, not projectors"""
|
|
|
|
ALLOWED_BASIS = "IXYZ"
|
|
|
|
with self.assertRaises(ValueError):
|
|
PauliArray.coerce_observable("0101")
|
|
for p in qi.pauli_basis(1):
|
|
obs = PauliArray.coerce_observable(p)
|
|
self.assertEqual(obs, {p.to_label(): 1})
|
|
|
|
@ddt.data("iXX", "012", "+/-")
|
|
def test_coerce_observable_invalid_str(self, basis):
|
|
"""Test coerce_observable for Pauli input"""
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray.coerce_observable(basis)
|
|
|
|
@ddt.data(1, 2, 3)
|
|
def test_coerce_observable_pauli(self, num_qubits):
|
|
"""Test coerce_observable for Pauli input"""
|
|
for p in qi.pauli_basis(num_qubits):
|
|
obs = ObservablesArray.coerce_observable(p)
|
|
self.assertEqual(obs, {p.to_label(): 1})
|
|
|
|
@ddt.data(0, 1, 2, 3)
|
|
def test_coerce_observable_phased_pauli(self, phase):
|
|
"""Test coerce_observable for phased Pauli input"""
|
|
pauli = qi.Pauli("IXYZ")
|
|
pauli.phase = phase
|
|
coeff = (-1j) ** phase
|
|
if phase % 2:
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray.coerce_observable(pauli)
|
|
else:
|
|
obs = ObservablesArray.coerce_observable(pauli)
|
|
self.assertIsInstance(obs, dict)
|
|
self.assertEqual(list(obs.keys()), ["IXYZ"])
|
|
np.testing.assert_allclose(
|
|
list(obs.values()), [coeff], err_msg=f"Wrong value for Pauli {pauli}"
|
|
)
|
|
|
|
@ddt.data("+IXYZ", "-IXYZ", "iIXYZ", "+iIXYZ", "-IXYZ")
|
|
def test_coerce_observable_phased_pauli_str(self, pauli):
|
|
"""Test coerce_observable for phased Pauli input"""
|
|
pauli = qi.Pauli(pauli)
|
|
coeff = (-1j) ** pauli.phase
|
|
if pauli.phase % 2:
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray.coerce_observable(pauli)
|
|
else:
|
|
obs = ObservablesArray.coerce_observable(pauli)
|
|
self.assertIsInstance(obs, dict)
|
|
self.assertEqual(list(obs.keys()), ["IXYZ"])
|
|
np.testing.assert_allclose(
|
|
list(obs.values()), [coeff], err_msg=f"Wrong value for Pauli {pauli}"
|
|
)
|
|
|
|
def test_coerce_observable_signed_sparse_pauli_op(self):
|
|
"""Test coerce_observable for SparsePauliOp input with phase paulis"""
|
|
op = qi.SparsePauliOp(["+I", "-X", "Y", "-Z"], [1, 2, 3, 4])
|
|
obs = ObservablesArray.coerce_observable(op)
|
|
self.assertIsInstance(obs, dict)
|
|
self.assertEqual(len(obs), 4)
|
|
self.assertEqual(sorted(obs.keys()), sorted(["I", "X", "Y", "Z"]))
|
|
np.testing.assert_allclose([obs[i] for i in ["I", "X", "Y", "Z"]], [1, -2, 3, -4])
|
|
|
|
def test_coerce_observable_zero_sparse_pauli_op(self):
|
|
"""Test coerce_observable for SparsePauliOp input with zero val coeffs"""
|
|
op = qi.SparsePauliOp(["I", "X", "Y", "Z"], [0, 0, 0, 1])
|
|
obs = ObservablesArray.coerce_observable(op)
|
|
self.assertIsInstance(obs, dict)
|
|
self.assertEqual(len(obs), 1)
|
|
self.assertEqual(sorted(obs.keys()), ["Z"])
|
|
self.assertEqual(obs["Z"], 1)
|
|
|
|
def test_coerce_observable_duplicate_sparse_pauli_op(self):
|
|
"""Test coerce_observable for SparsePauliOp with duplicate paulis"""
|
|
op = qi.SparsePauliOp(["XX", "-XX", "XX", "-XX"], [2, 1, 3, 2])
|
|
obs = ObservablesArray.coerce_observable(op)
|
|
self.assertIsInstance(obs, dict)
|
|
self.assertEqual(len(obs), 1)
|
|
self.assertEqual(list(obs.keys()), ["XX"])
|
|
self.assertEqual(obs["XX"], 2)
|
|
|
|
def test_coerce_observable_pauli_mapping(self):
|
|
"""Test coerce_observable for pauli-keyed Mapping input"""
|
|
mapping = dict(zip(qi.pauli_basis(1), range(1, 5)))
|
|
obs = ObservablesArray.coerce_observable(mapping)
|
|
target = {key.to_label(): val for key, val in mapping.items()}
|
|
self.assertEqual(obs, target)
|
|
|
|
def test_coerce_0d(self):
|
|
"""Test the coerce() method with 0-d input."""
|
|
obs = ObservablesArray.coerce("X")
|
|
self.assertEqual(obs.shape, ())
|
|
self.assertDictAlmostEqual(obs[()], {"X": 1})
|
|
|
|
obs = ObservablesArray.coerce({"I": 2})
|
|
self.assertEqual(obs.shape, ())
|
|
self.assertDictAlmostEqual(obs[()], {"I": 2})
|
|
|
|
obs = ObservablesArray.coerce(qi.SparsePauliOp(["X", "Y"], [1, 3]))
|
|
self.assertEqual(obs.shape, ())
|
|
self.assertDictAlmostEqual(obs[()], {"X": 1, "Y": 3})
|
|
|
|
def test_format_invalid_mapping_qubits(self):
|
|
"""Test an error is raised when different qubits in mapping keys"""
|
|
mapping = {"IX": 1, "XXX": 2}
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray.coerce_observable(mapping)
|
|
|
|
def test_format_invalid_mapping_basis(self):
|
|
"""Test an error is raised when keys contain invalid characters"""
|
|
mapping = {"XX": 1, "0Z": 2, "02": 3}
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray.coerce_observable(mapping)
|
|
|
|
def test_init_nested_list_str(self):
|
|
"""Test init with nested lists of str"""
|
|
obj = [["X", "Y", "Z"], ["0", "1", "+"]]
|
|
obs = ObservablesArray(obj)
|
|
self.assertEqual(obs.size, 6)
|
|
self.assertEqual(obs.shape, (2, 3))
|
|
|
|
def test_init_nested_list_sparse_pauli_op(self):
|
|
"""Test init with nested lists of SparsePauliOp"""
|
|
obj = [
|
|
[qi.SparsePauliOp(qi.random_pauli_list(2, 3, phase=False)) for _ in range(3)]
|
|
for _ in range(5)
|
|
]
|
|
obs = ObservablesArray(obj)
|
|
self.assertEqual(obs.size, 15)
|
|
self.assertEqual(obs.shape, (5, 3))
|
|
|
|
def test_init_single_sparse_pauli_op(self):
|
|
"""Test init with single SparsePauliOps"""
|
|
obj = qi.SparsePauliOp(qi.random_pauli_list(2, 3, phase=False))
|
|
obs = ObservablesArray(obj)
|
|
self.assertEqual(obs.size, 1)
|
|
self.assertEqual(obs.shape, ())
|
|
|
|
def test_init_pauli_list(self):
|
|
"""Test init with PauliList"""
|
|
obs = ObservablesArray(qi.pauli_basis(2))
|
|
self.assertEqual(obs.size, 16)
|
|
self.assertEqual(obs.shape, (16,))
|
|
|
|
def test_init_nested_pauli_list(self):
|
|
"""Test init with nested PauliList"""
|
|
obj = [qi.random_pauli_list(2, 3, phase=False) for _ in range(5)]
|
|
obs = ObservablesArray(obj)
|
|
self.assertEqual(obs.size, 15)
|
|
self.assertEqual(obs.shape, (5, 3))
|
|
|
|
def test_init_ragged_array(self):
|
|
"""Test init with ragged input"""
|
|
obj = [["X", "Y"], ["X", "Y", "Z"]]
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray(obj)
|
|
|
|
def test_init_validate_false(self):
|
|
"""Test init validate kwarg"""
|
|
obj = [["A", "B", "C"], ["D", "E", "F"]]
|
|
obs = ObservablesArray(obj, validate=False)
|
|
self.assertEqual(obs.shape, (2, 3))
|
|
self.assertEqual(obs.size, 6)
|
|
for i in range(2):
|
|
for j in range(3):
|
|
self.assertEqual(obs[i, j], obj[i][j])
|
|
|
|
def test_init_validate_true(self):
|
|
"""Test init validate kwarg"""
|
|
obj = [["A", "B", "C"], ["D", "E", "F"]]
|
|
with self.assertRaises(ValueError):
|
|
ObservablesArray(obj, validate=True)
|
|
|
|
@ddt.data(0, 1, 2, 3)
|
|
def test_size_and_shape_single(self, ndim):
|
|
"""Test size and shape method for size=1 array"""
|
|
obs = {"XX": 1}
|
|
for _ in range(ndim):
|
|
obs = [obs]
|
|
arr = ObservablesArray(obs, validate=False)
|
|
self.assertEqual(arr.size, 1, msg="Incorrect ObservablesArray.size")
|
|
self.assertEqual(arr.shape, (1,) * ndim, msg="Incorrect ObservablesArray.shape")
|
|
|
|
@ddt.data(0, 1, 2, 3)
|
|
def test_tolist_single(self, ndim):
|
|
"""Test tolist method for size=1 array"""
|
|
obs = {"XX": 1}
|
|
for _ in range(ndim):
|
|
obs = [obs]
|
|
arr = ObservablesArray(obs, validate=False)
|
|
ls = arr.tolist()
|
|
self.assertEqual(ls, obs)
|
|
|
|
@ddt.data(0, 1, 2, 3)
|
|
def test_array_single(self, ndim):
|
|
"""Test __array__ method for size=1 array"""
|
|
obs = {"XX": 1}
|
|
for _ in range(ndim):
|
|
obs = [obs]
|
|
arr = ObservablesArray(obs, validate=False)
|
|
nparr = np.array(arr)
|
|
self.assertEqual(nparr.dtype, object)
|
|
self.assertEqual(nparr.shape, arr.shape)
|
|
self.assertEqual(nparr.size, arr.size)
|
|
self.assertTrue(np.all(nparr == np.array(obs)))
|
|
|
|
@ddt.data(0, 1, 2, 3)
|
|
def test_getitem_single(self, ndim):
|
|
"""Test __getitem__ method for size=1 array"""
|
|
base_obs = {"XX": 1}
|
|
obs = base_obs
|
|
for _ in range(ndim):
|
|
obs = [obs]
|
|
arr = ObservablesArray(obs, validate=False)
|
|
idx = ndim * (0,)
|
|
item = arr[idx]
|
|
self.assertEqual(item, base_obs)
|
|
|
|
def test_tolist_1d(self):
|
|
"""Test tolist method"""
|
|
obj = ["A", "B", "C", "D"]
|
|
obs = ObservablesArray(obj, validate=False)
|
|
self.assertEqual(obs.tolist(), obj)
|
|
|
|
def test_tolist_2d(self):
|
|
"""Test tolist method"""
|
|
obj = [["A", "B", "C"], ["D", "E", "F"]]
|
|
obs = ObservablesArray(obj, validate=False)
|
|
self.assertEqual(obs.tolist(), obj)
|
|
|
|
def test_array_1d(self):
|
|
"""Test __array__ dunder method"""
|
|
obj = np.array(["A", "B", "C", "D"], dtype=object)
|
|
obs = ObservablesArray(obj, validate=False)
|
|
self.assertTrue(np.all(np.array(obs) == obj))
|
|
|
|
def test_array_2d(self):
|
|
"""Test __array__ dunder method"""
|
|
obj = np.array([["A", "B", "C"], ["D", "E", "F"]], dtype=object)
|
|
obs = ObservablesArray(obj, validate=False)
|
|
self.assertTrue(np.all(np.array(obs) == obj))
|
|
|
|
def test_getitem_1d(self):
|
|
"""Test __getitem__ for 1D array"""
|
|
obj = np.array(["A", "B", "C", "D"], dtype=object)
|
|
obs = ObservablesArray(obj, validate=False)
|
|
for i in range(obj.size):
|
|
self.assertEqual(obs[i], obj[i])
|
|
|
|
def test_getitem_2d(self):
|
|
"""Test __getitem__ for 2D array"""
|
|
obj = np.array([["A", "B", "C"], ["D", "E", "F"]], dtype=object)
|
|
obs = ObservablesArray(obj, validate=False)
|
|
for i in range(obj.shape[0]):
|
|
row = obs[i]
|
|
self.assertIsInstance(row, ObservablesArray)
|
|
self.assertEqual(row.shape, (3,))
|
|
self.assertTrue(np.all(np.array(row) == obj[i]))
|
|
|
|
def test_ravel(self):
|
|
"""Test ravel method"""
|
|
bases_flat = qi.pauli_basis(2).to_labels()
|
|
bases = [bases_flat[4 * i : 4 * (i + 1)] for i in range(4)]
|
|
obs = ObservablesArray(bases)
|
|
flat = obs.ravel()
|
|
self.assertEqual(flat.ndim, 1)
|
|
self.assertEqual(flat.shape, (16,))
|
|
self.assertEqual(flat.size, 16)
|
|
for (
|
|
i,
|
|
label,
|
|
) in enumerate(bases_flat):
|
|
self.assertEqual(flat[i], {label: 1})
|
|
|
|
def test_reshape(self):
|
|
"""Test reshape method"""
|
|
bases = qi.pauli_basis(2)
|
|
labels = np.array(bases.to_labels(), dtype=object)
|
|
obs = ObservablesArray(qi.pauli_basis(2))
|
|
|
|
def various_formats(shape):
|
|
# call reshape with a single argument
|
|
yield [shape]
|
|
yield [(-1,) + shape[1:]]
|
|
yield [np.array(shape)]
|
|
yield [list(shape)]
|
|
yield [list(map(np.int64, shape))]
|
|
yield [tuple(map(np.int64, shape))]
|
|
|
|
# call reshape with multiple arguments
|
|
yield shape
|
|
yield np.array(shape)
|
|
yield list(shape)
|
|
yield list(map(np.int64, shape))
|
|
yield tuple(map(np.int64, shape))
|
|
|
|
for shape in [(16,), (4, 4), (2, 4, 2), (2, 2, 2, 2), (1, 8, 1, 2)]:
|
|
with self.subTest(shape):
|
|
for input_shape in various_formats(shape):
|
|
obs_rs = obs.reshape(*input_shape)
|
|
self.assertEqual(obs_rs.shape, shape)
|
|
labels_rs = labels.reshape(shape)
|
|
for idx in np.ndindex(shape):
|
|
self.assertEqual(
|
|
obs_rs[idx],
|
|
{labels_rs[idx]: 1},
|
|
msg=f"failed for shape {shape} with input format {input_shape}",
|
|
)
|
|
|
|
def test_validate(self):
|
|
"""Test the validate method"""
|
|
ObservablesArray({"XX": 1}).validate()
|
|
ObservablesArray([{"XX": 1}] * 5).validate()
|
|
ObservablesArray([{"XX": 1}] * 15).reshape((3, 5)).validate()
|
|
|
|
obs = ObservablesArray([{"XX": 1}, {"XYZ": 1}], validate=False)
|
|
with self.assertRaisesRegex(ValueError, "number of qubits must be the same"):
|
|
obs.validate()
|