Add `apply_layout` method to `SparsePauliOp` (#10947)

* Add apply_layout method to SparsePauliOp

This commit adds a new method, `apply_layout`, to the `SparsePauliOp`
class. It takes in either a `TranspileLayout` object or a list of
indices that represent a layout transformation caused by the transpiler
and then returns a new SparsePauliOp object that applies a matching
transformation.

* Fix docs typo

* Update releasenotes/notes/sparse-pauli-op-apply-layout-43149125d29ad015.yaml

* Apply suggestions from code review

Co-authored-by: Kevin Hartman <kevin@hart.mn>

* Fix release note typo

---------

Co-authored-by: Kevin Hartman <kevin@hart.mn>
This commit is contained in:
Matthew Treinish 2023-10-10 14:46:40 -04:00 committed by GitHub
parent 8651d34ad9
commit 947e175edc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 135 additions and 0 deletions

View File

@ -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)

View File

@ -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`.

View File

@ -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()