diff --git a/docs/notebooks/twirling.ipynb b/docs/notebooks/twirling.ipynb new file mode 100644 index 0000000..0c24bbf --- /dev/null +++ b/docs/notebooks/twirling.ipynb @@ -0,0 +1,363 @@ +{ + "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": [ + "## What is twirling?\n", + "```\n", + "MISSING SCIENTIFIC/THEORETICAL EXPLANATION\n", + "```\n", + "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": [ + "## Operation-preserving twirls\n", + "```\n", + "MISSING SCIENTIFIC/THEORETICAL EXPLANATION\n", + "```\n", + "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": [ + "## Random twirling\n", + "```\n", + "MISSING SCIENTIFIC/THEORETICAL EXPLANATION\n", + "```\n", + "Finally, with these tools, we can create a simple [transpiler pass](https://docs.quantum.ibm.com/transpile/custom-transpiler-pass#transpiler-passes) `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": [ + "## Usage\n", + "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 +}