diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index b6f79af947..9eecbed2f0 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -14,6 +14,7 @@ N-Qubit Sparse Pauli Operator class. """ from __future__ import annotations +from typing import TYPE_CHECKING, List from collections import defaultdict from collections.abc import Mapping, Sequence, Iterable @@ -38,6 +39,10 @@ from qiskit.quantum_info.operators.symplectic.pauli_utils import pauli_basis from qiskit.quantum_info.operators.symplectic.pauli import Pauli +if TYPE_CHECKING: + from qiskit.transpiler.layout import TranspileLayout + + class SparsePauliOp(LinearOp): """Sparse N-qubit operator in a Pauli basis representation. @@ -1103,6 +1108,41 @@ class SparsePauliOp(LinearOp): return None if inplace else bound + def apply_layout( + self, layout: TranspileLayout | List[int], num_qubits: int | None = None + ) -> SparsePauliOp: + """Apply a transpiler layout to this :class:`~.SparsePauliOp` + + Args: + layout: Either a :class:`~.TranspileLayout` or a list of integers. + 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. If ``layout`` is a list of integers the permutation + specified will be applied without any expansion. + + + Returns: + A new :class:`.SparsePauliOp` with the provided layout applied + """ + from qiskit.transpiler.layout import TranspileLayout + + n_qubits = self.num_qubits + if isinstance(layout, TranspileLayout): + n_qubits = len(layout._output_qubit_list) + layout = layout.final_index_layout() + 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 + if any(x >= n_qubits for x in layout): + raise QiskitError("Provided layout contains indicies outside the number of qubits.") + new_op = type(self)("I" * n_qubits) + return new_op.compose(self, qargs=layout) + # Update docstrings for API docs generate_apidocs(SparsePauliOp) diff --git a/releasenotes/notes/sparse-pauli-op-apply-layout-43149125d29ad015.yaml b/releasenotes/notes/sparse-pauli-op-apply-layout-43149125d29ad015.yaml new file mode 100644 index 0000000000..2129e35428 --- /dev/null +++ b/releasenotes/notes/sparse-pauli-op-apply-layout-43149125d29ad015.yaml @@ -0,0 +1,32 @@ +--- +features: + - | + Added a new method, :meth:`~.SparsePauliOp.apply_layout`, + to the :class:~.SparsePauliOp` class. This method is used to apply + a :class:`~.TranspileLayout` layout from the transpiler + to a :class:~.SparsePauliOp` observable that was built for an + input circuit to the transpiler. This enables working with + :class:`~.BaseEstimator` implementations and local transpilation more + easily. For example:: + + from qiskit.circuit.library import RealAmplitudes + from qiskit.quantum_info import SparsePauliOp + from qiskit.primitives import BackendEstimator + from qiskit.compiler import transpile + from qiskit.providers.fake_provider import FakeNairobiV2 + + psi = RealAmplitudes(num_qubits=2, reps=2) + H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) + backend = FakeNairobiV2() + estimator = BackendEstimator(backend=backend, skip_transpilation=True) + + thetas = [0, 1, 1, 2, 3, 5] + transpiled_psi = transpile(psi, backend, optimization_level=3) + permuted_op = H1.apply_layout(transpiled_psi.layout) + res = estimator.run(transpiled_psi, permuted_op, thetas) + + where an input circuit is transpiled locally before it's passed to + :class:`~.BaseEstimator.run`. Transpilation expands the original + circuit from 2 to 7 qubits (the size of ``backend``) and permutes its layout, + which is then applied to ``H1`` using :meth:`~.SparsePauliOp.apply_layout` + to reflect the transformations performed by :func:`~.transpile`. diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py index f44da980c9..df0eb0d20f 100644 --- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py +++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py @@ -24,6 +24,10 @@ from qiskit.circuit import ParameterExpression, Parameter, ParameterVector from qiskit.circuit.parametertable import ParameterView from qiskit.quantum_info.operators import Operator, Pauli, PauliList, PauliTable, SparsePauliOp from qiskit.test import QiskitTestCase +from qiskit.circuit.library import EfficientSU2 +from qiskit.primitives import BackendEstimator +from qiskit.providers.fake_provider import FakeNairobiV2 +from qiskit.compiler.transpiler import transpile def pauli_mat(label): @@ -1040,6 +1044,65 @@ class TestSparsePauliOpMethods(QiskitTestCase): with self.assertRaisesRegex(ValueError, "incorrect number of operators"): op.paulis = PauliList([Pauli("XY"), Pauli("ZX"), Pauli("YZ")]) + def test_apply_layout_with_transpile(self): + """Test the apply_layout method with a transpiler layout.""" + psi = EfficientSU2(4, reps=4, entanglement="circular") + op = SparsePauliOp.from_list([("IIII", 1), ("IZZZ", 2), ("XXXI", 3)]) + backend = FakeNairobiV2() + transpiled_psi = transpile(psi, backend, optimization_level=3, seed_transpiler=12345) + permuted_op = op.apply_layout(transpiled_psi.layout) + identity_op = SparsePauliOp("I" * 7) + initial_layout = transpiled_psi.layout.initial_index_layout(filter_ancillas=True) + final_layout = transpiled_psi.layout.routing_permutation() + qargs = [final_layout[x] for x in initial_layout] + expected_op = identity_op.compose(op, qargs=qargs) + self.assertNotEqual(op, permuted_op) + self.assertEqual(permuted_op, expected_op) + + def test_permute_sparse_pauli_op_estimator_example(self): + """Test using the apply_layout method with an estimator workflow.""" + psi = EfficientSU2(4, reps=4, entanglement="circular") + op = SparsePauliOp.from_list([("IIII", 1), ("IZZZ", 2), ("XXXI", 3)]) + backend = FakeNairobiV2() + backend.set_options(seed_simulator=123) + estimator = BackendEstimator(backend=backend, skip_transpilation=True) + thetas = list(range(len(psi.parameters))) + transpiled_psi = transpile(psi, backend, optimization_level=3) + permuted_op = op.apply_layout(transpiled_psi.layout) + job = estimator.run(transpiled_psi, permuted_op, thetas) + res = job.result().values + np.testing.assert_allclose(res, [1.35351562], rtol=0.5, atol=0.2) + + def test_apply_layout_invalid_qubits_list(self): + """Test that apply_layout with an invalid qubit count raises.""" + op = SparsePauliOp.from_list([("YI", 2), ("XI", 1)]) + with self.assertRaises(QiskitError): + op.apply_layout([0, 1], 1) + + def test_apply_layout_invalid_layout_list(self): + """Test that apply_layout with an invalid layout list raises.""" + op = SparsePauliOp.from_list([("YI", 2), ("IX", 1)]) + with self.assertRaises(QiskitError): + op.apply_layout([0, 3], 2) + + def test_apply_layout_invalid_layout_list_no_num_qubits(self): + """Test that apply_layout with an invalid layout list raises.""" + op = SparsePauliOp.from_list([("YI", 2), ("XI", 1)]) + with self.assertRaises(QiskitError): + op.apply_layout([0, 2]) + + def test_apply_layout_layout_list_no_num_qubits(self): + """Test apply_layout with a layout list and no qubit count""" + op = SparsePauliOp.from_list([("YI", 2), ("XI", 1)]) + res = op.apply_layout([1, 0]) + self.assertEqual(SparsePauliOp.from_list([("IY", 2), ("IX", 1)]), res) + + def test_apply_layout_layout_list_and_num_qubits(self): + """Test apply_layout with a layout list and qubit count""" + op = SparsePauliOp.from_list([("YI", 2), ("XI", 1)]) + res = op.apply_layout([4, 0], 5) + self.assertEqual(SparsePauliOp.from_list([("IIIIY", 2), ("IIIIX", 1)]), res) + if __name__ == "__main__": unittest.main()