Skip to content

Commit

Permalink
docs(notebooks): add Pauli twirling guide
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrorrivero committed Feb 28, 2024
1 parent e7d8b98 commit 14dac3e
Showing 1 changed file with 350 additions and 0 deletions.
350 changes: 350 additions & 0 deletions docs/notebooks/twirling.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Abdullah Ash Saki** \n",
"Enabling Technologies Researcher @ IBM Quantum \n",
"saki@ibm.com\n",
"\n",
"**Pedro Rivero** \n",
"Technical Lead @ IBM Quantum \n",
"pedro.rivero@ibm.com"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Pauli Twirling"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let us begin by introducing an auxiliary class `PauliTwirl`, to represent a single Pauli twirl:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from numpy import exp, ndarray\n",
"from numpy.typing import ArrayLike\n",
"from qiskit.circuit import QuantumRegister\n",
"from qiskit.circuit.library import PauliGate\n",
"from qiskit.dagcircuit import DAGCircuit, DAGOpNode\n",
"\n",
"\n",
"class PauliTwirl:\n",
" \"\"\"Pauli twirl.\n",
"\n",
" This class holds information about a Pauli twirl, independently\n",
" of what operation it is later applied to. Therefore, applying\n",
" the represented twirl to an arbitrary operation has no guaranty\n",
" of preserving such operation's original action.\n",
"\n",
" Args:\n",
" pre: Pauli gate to apply before the twirled operation.\n",
" post: Pauli gate to apply after the twirled operation.\n",
" phase: global phase induced by the twirling.\n",
" \"\"\"\n",
"\n",
" def __init__(self, pre: str, post: str, phase: float = 0.0) -> None:\n",
" self.pre = PauliGate(pre)\n",
" self.post = PauliGate(post)\n",
" self.phase = float(phase)\n",
" if self.pre.num_qubits != self.post.num_qubits:\n",
" raise ValueError(\n",
" \"Twirling pre and post operations don't apply to the same number of qubits.\"\n",
" )\n",
"\n",
" @property\n",
" def num_qubits(self) -> int:\n",
" \"\"\"Number of qubits that the twirl applies to.\"\"\"\n",
" return self.pre.num_qubits\n",
"\n",
" def apply_to_node(self, node: DAGOpNode) -> DAGCircuit:\n",
" \"\"\"Apply twirl to input DAG operation node.\"\"\"\n",
" dag = DAGCircuit()\n",
" qubits = QuantumRegister(self.num_qubits)\n",
" dag.add_qreg(qubits)\n",
" dag.apply_operation_back(self.pre, qubits)\n",
" dag.apply_operation_back(node.op, qubits)\n",
" dag.apply_operation_back(self.post, qubits)\n",
" dag.global_phase += self.phase\n",
" return dag\n",
"\n",
" def apply_to_unitary(self, unitary: ArrayLike) -> ndarray:\n",
" \"\"\"Apply twirl to input unitary.\"\"\"\n",
" pre = self.pre.to_matrix()\n",
" post = self.post.to_matrix()\n",
" phase_factor = exp(1j * self.phase)\n",
" return (post @ unitary @ pre) * phase_factor\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Next we will create a helper function `generate_pauli_twirls` to compute all operation-preserving twirls for a given unitary matrix numerically:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from collections.abc import Iterator\n",
"from itertools import product\n",
"from numpy import allclose, angle, eye, isclose, ndarray\n",
"\n",
"\n",
"def generate_pauli_twirls(unitary: ndarray) -> Iterator[PauliTwirl]:\n",
" \"\"\"Generate operation-preserving Pauli twirls for input unitary.\n",
"\n",
" Args:\n",
" unitary: the unitary to compute twirls for.\n",
"\n",
" Yields:\n",
" Twirls preserving the unitary operation. Qubit order is given by the input.\n",
" \"\"\"\n",
" dimension = unitary.shape[0]\n",
" num_qubits = dimension.bit_length() - 1 # Note: dimension == 2**num_qubits\n",
" n_qubit_paulis = (\"\".join(pauli) for pauli in product(\"IXYZ\", repeat=num_qubits))\n",
" for pre, post in product(n_qubit_paulis, repeat=2):\n",
" twirl = PauliTwirl(pre, post, phase=0.0)\n",
" twirled = twirl.apply_to_unitary(unitary)\n",
" check = twirled.conj().T @ unitary\n",
" phase_factor = check[0, 0]\n",
" if not isclose(phase_factor, 0) and allclose(check / phase_factor, eye(dimension)):\n",
" yield PauliTwirl(pre=pre, post=post, phase=angle(phase_factor))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, with these tools, we can create a simple transpiler pass `TwoQubitPauliTwirlPass` to apply Pauli twirling to an input `DAGCircuit`:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"from numpy.random import default_rng\n",
"from qiskit.circuit import Operation, Gate\n",
"from qiskit.dagcircuit import DAGCircuit\n",
"from qiskit.transpiler import TransformationPass\n",
"\n",
"\n",
"class TwoQubitPauliTwirlPass(TransformationPass):\n",
" \"\"\"Pauli twirl two-qubit gates in input circuit randomly.\n",
"\n",
" Both non-unitary and parametrized gates are not supported and will be skipped.\n",
"\n",
" Args:\n",
" seed: seed for random number generator.\n",
" \"\"\"\n",
"\n",
" def __init__(self, *, seed: int | None = None):\n",
" super().__init__()\n",
" self._rng = default_rng(seed)\n",
"\n",
" def run(self, dag: DAGCircuit):\n",
" \"\"\"Pauli twirl target gates randomly for input DAGCircuit inplace.\"\"\"\n",
" target_nodes = (node for node in dag.op_nodes() if self._is_target_op(node.op))\n",
" for node in target_nodes:\n",
" twirl = self._get_random_twirl(node.op)\n",
" twirl_dag = twirl.apply_to_node(node)\n",
" dag.substitute_node_with_dag(node, twirl_dag)\n",
" return dag\n",
"\n",
" def _is_target_op(self, op: Operation) -> bool:\n",
" \"\"\"Check whether operation should be twirled or not.\"\"\"\n",
" if op.num_qubits != 2:\n",
" return False # Note: Only twirl two-qubit gates\n",
" if not isinstance(op, Gate):\n",
" return False # Note: Skip non-gate nodes (e.g. barriers, measurements)\n",
" if op.is_parameterized():\n",
" return False # Note: Skip parametrized gates\n",
" return True\n",
"\n",
" def _get_random_twirl(self, gate: Gate) -> PauliTwirl:\n",
" \"\"\"Get random twirl for the input gate.\"\"\"\n",
" twirls = generate_pauli_twirls(gate.to_matrix())\n",
" return self._rng.choice(list(twirls))\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using Qiskit's `PassManager` we can now run a simple example:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"global phase: π\n",
" ┌────────────┐ ┌────────────┐\n",
"q_0: ┤0 ├──■──┤0 ├\n",
" │ Pauli(ZX) │┌─┴─┐│ Pauli(YY) │\n",
"q_1: ┤1 ├┤ X ├┤1 ├\n",
" └────────────┘└───┘└────────────┘\n"
]
}
],
"source": [
"from qiskit import QuantumCircuit\n",
"from qiskit.transpiler import PassManager\n",
"\n",
"circuit = QuantumCircuit(2)\n",
"circuit.cx(0, 1)\n",
"\n",
"pass_manager = PassManager(TwoQubitPauliTwirlPass(seed=0))\n",
"twirled_circuit = pass_manager.run(circuit)\n",
"\n",
"print(twirled_circuit)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Which can be further decomposed into single-qubit paulis:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"global phase: π\n",
" ┌───┐ ┌───┐\n",
"q_0: ┤ X ├──■──┤ Y ├\n",
" ├───┤┌─┴─┐├───┤\n",
"q_1: ┤ Z ├┤ X ├┤ Y ├\n",
" └───┘└───┘└───┘\n"
]
}
],
"source": [
"print(twirled_circuit.decompose(\"pauli\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And we can see how the unitary is preserved:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Original: \n",
" [[1. 0. 0. 0.]\n",
" [0. 0. 0. 1.]\n",
" [0. 0. 1. 0.]\n",
" [0. 1. 0. 0.]]\n",
"Twirled: \n",
" [[ 1. -0. -0. -0.]\n",
" [-0. -0. -0. 1.]\n",
" [-0. -0. 1. -0.]\n",
" [-0. 1. -0. -0.]]\n"
]
}
],
"source": [
"from numpy import isclose, pi\n",
"from qiskit.circuit.library import CXGate\n",
"\n",
"twirl = PauliTwirl(\"ZX\", \"YY\", phase=pi)\n",
"\n",
"cx_unitary = CXGate().to_matrix()\n",
"twirled_unitary = twirl.apply_to_unitary(cx_unitary)\n",
"\n",
"print(\"Original: \\n\", cx_unitary.real)\n",
"print(\"Twirled: \\n\", twirled_unitary.real)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Mask (isclose): \n",
" [[ True True True True]\n",
" [ True True True True]\n",
" [ True True True True]\n",
" [ True True True True]]\n"
]
}
],
"source": [
"print(\"Mask (isclose): \\n\", isclose(twirled_unitary, cx_unitary))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## References\n",
"1. Wallman et al., _Noise tailoring for scalable quantum computation via randomized compiling_, [Phys. Rev. A 94, 052325](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.94.052325)\n",
"2. Minev, _A tutorial on tailoring quantum noise - Twirling 101_, [Online](https://www.zlatko-minev.com/blog/twirling)\n",
"3. ..."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "quantum-enablement",
"language": "python",
"name": "quantum-enablement"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

0 comments on commit 14dac3e

Please sign in to comment.