From 5b25adf18db3d2643a047e913eae33ab784a545e Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 18 Apr 2024 23:55:26 -0400 Subject: [PATCH 1/8] Add ObservablesArray.apply_layout() --- .../containers/observables_array.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py index 0d0322dc6a3c..b53dd5083956 100644 --- a/qiskit/primitives/containers/observables_array.py +++ b/qiskit/primitives/containers/observables_array.py @@ -27,6 +27,8 @@ from numpy.typing import ArrayLike from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp +from qiskit.exceptions import QiskitError +from qiskit.transpiler import TranspileLayout from .object_array import object_array from .shape import ShapedMixin, shape_tuple @@ -97,6 +99,83 @@ def __repr__(self): array = np.array2string(self._array, prefix=prefix, suffix=suffix, threshold=50) return prefix + array + suffix + def apply_layout( + self, layout: TranspileLayout | list[int] | None, num_qubits: int | None = None + ) -> ObservablesArray: + """Apply a transpiler layout to this observables array. + + Args: + layout: Either a :class:`~.TranspileLayout`, a list of integers, or ``None``. + If both layout and ``num_qubits`` are none, a copy of the operator is + returned. + num_qubits: The number of qubits to expand the operator to. If not + provided then, if ``layout`` is a :class:`~.TranspileLayout`, the + number of the transpiler output circuit qubits will be used by + default. However, if ``layout`` is a list of integers, the permutation + specified will be applied without any expansion. If layout is + ``None``, the operator will be expanded to the given number of qubits. + + Returns: + A new observables array with the provided layout applied. + """ + if layout is None and num_qubits is None or self.size == 0: + return ObservablesArray(self._array, copy=True, validate=False) + + # Determine index layout for each observable + obs_num_qubits = len(next(iter(self._array.flat[0]))) + + if layout is None: + n_qubits = obs_num_qubits + layout = list(range(n_qubits)) + elif isinstance(layout, TranspileLayout): + n_qubits = len(layout._output_qubit_list) + layout = layout.final_index_layout() + if layout is not None and any(x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indicies outside the number of qubits.") + + if num_qubits is not None: + if num_qubits < n_qubits: + raise QiskitError( + f"The input num_qubits is too small, a {num_qubits} qubit layout cannot be " + f"applied to a {n_qubits} qubit operator" + ) + n_qubits = num_qubits + + # Check if layout is trivial mapping + trivial_layout = layout == list(range(obs_num_qubits)) + + # If trivial layout and no qubit padding we return a copy + if trivial_layout and n_qubits == obs_num_qubits: + return ObservablesArray(self._array, copy=True, validate=False) + + # Otherwise we need to pad and possible remap all dict keys + # This is super inefficient, and we really need a new data + # structure to avoid all this iteration and string manipulation + if trivial_layout: + pad = (n_qubits - obs_num_qubits) * "I" + + def _key_fn(key): + return pad + key + + else: + + def _key_fn(key): + new_key = n_qubits * ["I"] + for char, qubit in zip(reversed(key), layout): + # Qubit position is from end of string + new_key[n_qubits - 1 - qubit] = char + return "".join(new_key) + + mapped_array = np.empty_like(self._array) + for idx, observable in np.ndenumerate(self._array): + # Remap observable + new_observable = {} + for key, val in observable.items(): + new_observable[_key_fn(key)] = val + mapped_array[idx] = new_observable + + return ObservablesArray(mapped_array, copy=False, validate=False) + def tolist(self) -> list: """Convert to a nested list""" return self._array.tolist() From 55816754552804ab8a907402db4c6ce784e7703c Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Thu, 2 May 2024 23:37:46 -0400 Subject: [PATCH 2/8] add apply_layout() tests --- .../containers/observables_array.py | 14 ++-- .../containers/test_observables_array.py | 71 ++++++++++++++++++- .../transpiler/test_transpile_layout.py | 3 - 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py index 426406383b5b..7b396858d7cc 100644 --- a/qiskit/primitives/containers/observables_array.py +++ b/qiskit/primitives/containers/observables_array.py @@ -27,7 +27,6 @@ from numpy.typing import ArrayLike from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp -from qiskit.exceptions import QiskitError from qiskit.transpiler import TranspileLayout from .object_array import object_array @@ -113,10 +112,15 @@ def apply_layout( number of the transpiler output circuit qubits will be used by default. However, if ``layout`` is a list of integers, the permutation specified will be applied without any expansion. If layout is - ``None``, the operator will be expanded to the given number of qubits. + ``None``, the operator will be expanded to the given ``num_qubits``. Returns: A new observables array with the provided layout applied. + + Raises: + ValueError: If a :class:`~.TranspileLayout` is given that maps to qubit numbers that + are larger than the number of qubits in this array. + ValueError: If ``num_qubits`` is less than the number of """ if layout is None and num_qubits is None or self.size == 0: return ObservablesArray(self._array, copy=True, validate=False) @@ -131,11 +135,13 @@ def apply_layout( n_qubits = len(layout._output_qubit_list) layout = layout.final_index_layout() if layout is not None and any(x >= n_qubits for x in layout): - raise QiskitError("Provided layout contains indicies outside the number of qubits.") + raise ValueError("Provided layout contains indices outside the number of qubits.") + else: + n_qubits = len(layout) if num_qubits is not None: if num_qubits < n_qubits: - raise QiskitError( + raise ValueError( f"The input num_qubits is too small, a {num_qubits} qubit layout cannot be " f"applied to a {n_qubits} qubit operator" ) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index 5a8513a5ed9c..70d9df8df44f 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -16,9 +16,14 @@ import ddt import numpy as np +from qiskit.circuit import QuantumRegister, QuantumCircuit import qiskit.quantum_info as qi +from qiskit.primitives import StatevectorEstimator from qiskit.primitives.containers.observables_array import ObservablesArray -from test import QiskitTestCase # pylint: disable=wrong-import-order +from qiskit.transpiler import TranspileLayout, Layout +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.providers.fake_provider import GenericBackendV2 +from test import QiskitTestCase @ddt.ddt @@ -357,3 +362,67 @@ def test_validate(self): obs = ObservablesArray([{"XX": 1}, {"XYZ": 1}], validate=False) with self.assertRaisesRegex(ValueError, "number of qubits must be the same"): obs.validate() + + def test_apply_layout_errors(self): + """Test the apply_layout() method errors as expected.""" + obs = ObservablesArray({"XYZ": 1}) + layout = [1, 0, 2] + with self.assertRaisesRegex(ValueError, "num_qubits is too small"): + obs.apply_layout(layout, 2) + + def test_apply_layout_trivial(self): + """Test the apply_layout() method for trivial inputs.""" + obs = ObservablesArray("XYZ") + self.assertEqual(obs.apply_layout(None).tolist(), obs.tolist()) + self.assertEqual(obs.apply_layout(None, 3).tolist(), obs.tolist()) + + def test_apply_layout_padding(self): + """Test the apply_layout() method with identity padding.""" + obs = ObservablesArray({"XYZ": 1, "XYY": 2}) + self.assertEqual(obs.apply_layout(None, 5).tolist(), {"IIXYZ": 1, "IIXYY": 2}) + self.assertEqual(obs.apply_layout([2, 1, 0], 5).tolist(), {"IIZYX": 1, "IIYYX": 2}) + self.assertEqual(obs.apply_layout([4, 1, 0], 5).tolist(), {"ZIIYX": 1, "YIIYX": 2}) + + def test_apply_layout_permutations(self): + """Test the apply_layout() method under permutations.""" + obs_in = ObservablesArray([{"XYZ": 1}]) + obs_out = obs_in.apply_layout([0, 1, 2]) + self.assertEqual(obs_out.tolist(), [{"XYZ": 1}]) + + obs_in = ObservablesArray([{"XYZ": 1}]) + obs_out = obs_in.apply_layout([1, 0, 2]) + self.assertEqual(obs_out.tolist(), [{"XZY": 1}]) + + obs_in = ObservablesArray([{"XYZ": 1}]) + obs_out = obs_in.apply_layout([2, 1, 0]) + self.assertEqual(obs_out.tolist(), [{"ZYX": 1}]) + + def test_apply_layout_from_transpiled_circuit(self): + """Test that the apply_layout() method interfaces properly with transpiler routing.""" + + circuit = QuantumCircuit(5) + circuit.h(0) + circuit.cx(0, 4) + circuit.cx(3, 0) + for idx in range(5): + circuit.rz(idx / 5, idx) + circuit.cx(0, 4) + circuit.cx(2, 1) + circuit.h(0) + + obs = ObservablesArray( + [["I" * idx + pauli + "I" * (4 - idx) for pauli in "XYZ"] for idx in range(5)] + ) + + backend = GenericBackendV2(num_qubits=5, coupling_map=[[idx, idx + 1] for idx in range(4)]) + pm = generate_preset_pass_manager(optimization_level=1, backend=backend) + isa_circuit = pm.run(circuit) + isa_obs = obs.apply_layout(isa_circuit.layout) + + # sanity check that we have a non-trivial layout + self.assertNotEqual(isa_circuit.layout.final_index_layout(), [0, 1, 2, 3, 4]) + + # the estimates before and after the rerouting should be the same for any observables + estimator = StatevectorEstimator() + result = estimator.run([(circuit, obs), (isa_circuit, isa_obs)]).result() + np.testing.assert_allclose(result[0].data.evs, result[1].data.evs, rtol=0, atol=1e-10) diff --git a/test/python/transpiler/test_transpile_layout.py b/test/python/transpiler/test_transpile_layout.py index 01e06b57fee4..0ad1cfd40541 100644 --- a/test/python/transpiler/test_transpile_layout.py +++ b/test/python/transpiler/test_transpile_layout.py @@ -189,9 +189,6 @@ def test_routing_permutation(self): { qr[0]: 2, qr[1]: 4, - qr[2]: 1, - qr[3]: 0, - qr[4]: 3, } ) layout_obj = TranspileLayout( From f8623db3676a0302cfb661625e940acc5b62bad9 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Fri, 3 May 2024 00:09:30 -0400 Subject: [PATCH 3/8] fix linting --- test/python/primitives/containers/test_observables_array.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index 70d9df8df44f..f20e103e2fb4 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -16,11 +16,10 @@ import ddt import numpy as np -from qiskit.circuit import QuantumRegister, QuantumCircuit +from qiskit.circuit import QuantumCircuit import qiskit.quantum_info as qi from qiskit.primitives import StatevectorEstimator from qiskit.primitives.containers.observables_array import ObservablesArray -from qiskit.transpiler import TranspileLayout, Layout from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.providers.fake_provider import GenericBackendV2 from test import QiskitTestCase From b93e0cca49213e0aed259d25b9d7376fb6803911 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 May 2024 06:12:17 -0400 Subject: [PATCH 4/8] Revert test changes in test_transpile_layout This commit reverts accidental changes to the test module test_transpile_layout. These changes were causing the test to fail because the exepected layout was no longer complete. --- test/python/transpiler/test_transpile_layout.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/python/transpiler/test_transpile_layout.py b/test/python/transpiler/test_transpile_layout.py index 0ad1cfd40541..01e06b57fee4 100644 --- a/test/python/transpiler/test_transpile_layout.py +++ b/test/python/transpiler/test_transpile_layout.py @@ -189,6 +189,9 @@ def test_routing_permutation(self): { qr[0]: 2, qr[1]: 4, + qr[2]: 1, + qr[3]: 0, + qr[4]: 3, } ) layout_obj = TranspileLayout( From e6559cc71eca590504157cd648b36f20007dfbdc Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 May 2024 06:52:21 -0400 Subject: [PATCH 5/8] Fix lint --- test/python/primitives/containers/test_observables_array.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index f20e103e2fb4..a525d5a5aef5 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -22,6 +22,7 @@ from qiskit.primitives.containers.observables_array import ObservablesArray from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.providers.fake_provider import GenericBackendV2 + from test import QiskitTestCase From 1dff0346f99445950dc9705eb79ed9fb674ed132 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 May 2024 08:26:01 -0400 Subject: [PATCH 6/8] Appease pylint's arbitrary import order rules --- test/python/primitives/containers/test_observables_array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index a525d5a5aef5..9c3546a7786c 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -12,6 +12,8 @@ """Test ObservablesArray""" +from test import QiskitTestCase + import itertools as it import ddt import numpy as np @@ -23,8 +25,6 @@ from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.providers.fake_provider import GenericBackendV2 -from test import QiskitTestCase - @ddt.ddt class ObservablesArrayTestCase(QiskitTestCase): From 94bf141b5616cb8b39b2e8a6479e6b763ea6aec4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 8 May 2024 09:54:06 -0400 Subject: [PATCH 7/8] Fix exception raising when integer layout is not a permutation --- qiskit/primitives/containers/observables_array.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/primitives/containers/observables_array.py b/qiskit/primitives/containers/observables_array.py index 7b396858d7cc..6ac30e18cd0d 100644 --- a/qiskit/primitives/containers/observables_array.py +++ b/qiskit/primitives/containers/observables_array.py @@ -26,6 +26,7 @@ import numpy as np from numpy.typing import ArrayLike +from qiskit.exceptions import QiskitError from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp from qiskit.transpiler import TranspileLayout @@ -118,9 +119,9 @@ def apply_layout( A new observables array with the provided layout applied. Raises: - ValueError: If a :class:`~.TranspileLayout` is given that maps to qubit numbers that + QiskitError: If a :class:`~.TranspileLayout` is given that maps to qubit numbers that are larger than the number of qubits in this array. - ValueError: If ``num_qubits`` is less than the number of + QiskitError: If ``num_qubits`` is less than the number of """ if layout is None and num_qubits is None or self.size == 0: return ObservablesArray(self._array, copy=True, validate=False) @@ -134,18 +135,18 @@ def apply_layout( elif isinstance(layout, TranspileLayout): n_qubits = len(layout._output_qubit_list) layout = layout.final_index_layout() - if layout is not None and any(x >= n_qubits for x in layout): - raise ValueError("Provided layout contains indices outside the number of qubits.") else: n_qubits = len(layout) if num_qubits is not None: if num_qubits < n_qubits: - raise ValueError( + raise QiskitError( f"The input num_qubits is too small, a {num_qubits} qubit layout cannot be " f"applied to a {n_qubits} qubit operator" ) n_qubits = num_qubits + if layout is not None and any(x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indices outside the number of qubits.") # Check if layout is trivial mapping trivial_layout = layout == list(range(obs_num_qubits)) From 807333bcb973e31ecd8acc32e8e6c3062251d568 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wood" Date: Wed, 8 May 2024 10:03:54 -0400 Subject: [PATCH 8/8] Fix test for error type --- test/python/primitives/containers/test_observables_array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/python/primitives/containers/test_observables_array.py b/test/python/primitives/containers/test_observables_array.py index 9c3546a7786c..e89b6834efe8 100644 --- a/test/python/primitives/containers/test_observables_array.py +++ b/test/python/primitives/containers/test_observables_array.py @@ -18,6 +18,7 @@ import ddt import numpy as np +from qiskit.exceptions import QiskitError from qiskit.circuit import QuantumCircuit import qiskit.quantum_info as qi from qiskit.primitives import StatevectorEstimator @@ -367,7 +368,7 @@ def test_apply_layout_errors(self): """Test the apply_layout() method errors as expected.""" obs = ObservablesArray({"XYZ": 1}) layout = [1, 0, 2] - with self.assertRaisesRegex(ValueError, "num_qubits is too small"): + with self.assertRaisesRegex(QiskitError, "num_qubits is too small"): obs.apply_layout(layout, 2) def test_apply_layout_trivial(self):