Skip to content

Commit

Permalink
Add apply_layout method to SparsePauliOp (#10947)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Fix release note typo

---------

Co-authored-by: Kevin Hartman <[email protected]>
  • Loading branch information
mtreinish and kevinhartman authored Oct 10, 2023
1 parent 8651d34 commit 947e175
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 0 deletions.
40 changes: 40 additions & 0 deletions qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

from __future__ import annotations
from typing import TYPE_CHECKING, List

from collections import defaultdict
from collections.abc import Mapping, Sequence, Iterable
Expand All @@ -38,6 +39,10 @@
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.
Expand Down Expand Up @@ -1103,6 +1108,41 @@ def assign_parameters(

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)
Original file line number Diff line number Diff line change
@@ -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`.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
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):
Expand Down Expand Up @@ -1040,6 +1044,65 @@ def test_paulis_setter_rejects_bad_inputs(self):
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()

0 comments on commit 947e175

Please sign in to comment.