Skip to content

Commit

Permalink
Add .apply_layout() to Pauli class
Browse files Browse the repository at this point in the history
  • Loading branch information
ElePT committed Mar 22, 2024
1 parent f713c78 commit 45f59e2
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 1 deletion.
44 changes: 44 additions & 0 deletions qiskit/quantum_info/operators/symplectic/pauli.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
if TYPE_CHECKING:
from qiskit.quantum_info.operators.symplectic.clifford import Clifford
from qiskit.quantum_info.operators.symplectic.pauli_list import PauliList
from qiskit.transpiler.layout import TranspileLayout


class Pauli(BasePauli):
Expand Down Expand Up @@ -697,6 +698,49 @@ def _from_circuit(cls, instr):
ret = ret.compose(next_instr, qargs=qargs)
return ret._z, ret._x, ret._phase

def apply_layout(
self, layout: TranspileLayout | list[int] | None, num_qubits: int | None = None
) -> Pauli:
"""Apply a transpiler layout to this :class:`~.Pauli`
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. 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 :class:`.Pauli` with the provided layout applied
"""
from qiskit.transpiler.layout import TranspileLayout

if layout is None and num_qubits is None:
return self.copy()

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 layout is not None and any(x >= n_qubits for x in layout):
raise QiskitError("Provided layout contains indices outside the number of qubits.")
if layout is None:
layout = list(range(self.num_qubits))
new_op = type(self)("I" * n_qubits)
return new_op.compose(self, qargs=layout)


# Update docstrings for API docs
generate_apidocs(Pauli)
31 changes: 31 additions & 0 deletions releasenotes/notes/pauli-apply-layout-cdcbc1bce724a150.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
features_quantum_info:
- |
Added a new :meth:`~.Pauli.apply_layout` method that is equivalent to
:meth:`~.SparsePauliOp.apply_layout`. This method is used to apply
a :class:`~.TranspileLayout` layout from the transpiler to a :class:~.Pauli`
observable that was built for an input circuit. This enables working with
:class:`~.BaseEstimator` / :class:`~.BaseEstimatorV2` implementations and
local transpilation when the input is of type :class:`~.Pauli`. For example::
from qiskit.circuit.library import RealAmplitudes
from qiskit.primitives import BackendEstimatorV2
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import Pauli
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
psi = RealAmplitudes(num_qubits=2, reps=2)
H1 = Pauli("XI")
backend = GenericBackendV2(num_qubits=7)
estimator = BackendEstimatorV2(backend=backend)
thetas = [0, 1, 1, 2, 3, 5]
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
transpiled_psi = pm.run(psi)
permuted_op = H1.apply_layout(transpiled_psi.layout)
res = estimator.run([(transpiled_psi, permuted_op, thetas)]).result()
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:`~.Pauli.apply_layout`
to reflect the transformations performed by ``pm.run()``.
89 changes: 88 additions & 1 deletion test/python/quantum_info/operators/symplectic/test_pauli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2020.
# (C) Copyright IBM 2017, 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
Expand Down Expand Up @@ -36,10 +36,16 @@
CZGate,
CYGate,
SwapGate,
EfficientSU2,
)
from qiskit.circuit.library.generalized_gates import PauliGate
from qiskit.compiler.transpiler import transpile
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.primitives import BackendEstimator
from qiskit.quantum_info.random import random_clifford, random_pauli
from qiskit.quantum_info.operators import Pauli, Operator
from qiskit.utils import optionals

from test import QiskitTestCase # pylint: disable=wrong-import-order


Expand Down Expand Up @@ -495,6 +501,87 @@ def test_circuit_with_bit(self):

self.assertEqual(value, target)

def test_apply_layout_with_transpile(self):
"""Test the apply_layout method with a transpiler layout."""
psi = EfficientSU2(4, reps=4, entanglement="circular")
op = Pauli("IZZZ")
backend = GenericBackendV2(num_qubits=7)
transpiled_psi = transpile(psi, backend, optimization_level=3, seed_transpiler=12345)
permuted_op = op.apply_layout(transpiled_psi.layout)
identity_op = Pauli("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_pauli_estimator_example(self):
"""Test using the apply_layout method with an estimator workflow."""
psi = EfficientSU2(4, reps=4, entanglement="circular")
op = Pauli("XXXI")
backend = GenericBackendV2(num_qubits=7, seed=0)
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
if optionals.HAS_AER:
np.testing.assert_allclose(res, [0.20898438], rtol=0.5, atol=0.2)
else:
np.testing.assert_allclose(res, [0.15820312], 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 = Pauli("YI")
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 = Pauli("YI")
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 = Pauli("YI")
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 = Pauli("YI")
res = op.apply_layout([1, 0])
self.assertEqual(Pauli("IY"), res)

def test_apply_layout_layout_list_and_num_qubits(self):
"""Test apply_layout with a layout list and qubit count"""
op = Pauli("YI")
res = op.apply_layout([4, 0], 5)
self.assertEqual(Pauli("IIIIY"), res)

def test_apply_layout_null_layout_no_num_qubits(self):
"""Test apply_layout with a null layout"""
op = Pauli("IZ")
res = op.apply_layout(layout=None)
self.assertEqual(op, res)

def test_apply_layout_null_layout_and_num_qubits(self):
"""Test apply_layout with a null layout a num_qubits provided"""
op = Pauli("IZ")
res = op.apply_layout(layout=None, num_qubits=5)
# this should expand the operator
self.assertEqual(Pauli("IIIIZ"), res)

def test_apply_layout_null_layout_invalid_num_qubits(self):
"""Test apply_layout with a null layout and num_qubits smaller than capable"""
op = Pauli("IZ")
with self.assertRaises(QiskitError):
op.apply_layout(layout=None, num_qubits=1)


if __name__ == "__main__":
unittest.main()

0 comments on commit 45f59e2

Please sign in to comment.