mirror of https://github.com/Qiskit/qiskit.git
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:
parent
8651d34ad9
commit
947e175edc
|
@ -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)
|
||||
|
|
|
@ -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`.
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue