diff --git a/docs/_book.yaml b/docs/_book.yaml index e35fe023c5f..66ffd8721d9 100644 --- a/docs/_book.yaml +++ b/docs/_book.yaml @@ -107,6 +107,8 @@ upper_tabs: path: /cirq/transform/transformers - title: "Custom Transformers" path: /cirq/transform/custom_transformers + - title: "Routing as a Transformer" + path: /cirq/transform/routing_transformer - name: "Hardware" contents: diff --git a/docs/transform/_index.yaml b/docs/transform/_index.yaml index e289d7b6663..e0e6fdb9215 100644 --- a/docs/transform/_index.yaml +++ b/docs/transform/_index.yaml @@ -15,3 +15,6 @@ landing_page: - heading: Custom Transformers description: Write your own Transformer with decorators, primitives and decompositions. path: /cirq/transform/custom_transformers + - heading: Routing as a Transformer + description: Qubit Routing utilities for easily executing circuits on hardware. + path: /cirq/transform/routing_transformer diff --git a/docs/transform/routing_transformer.ipynb b/docs/transform/routing_transformer.ipynb new file mode 100644 index 00000000000..39cb1827148 --- /dev/null +++ b/docs/transform/routing_transformer.ipynb @@ -0,0 +1,471 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellView": "form", + "id": "XI1k_z6yvbPq" + }, + "outputs": [], + "source": [ + "# @title Copyright 2022 The Cirq Developers\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZQlrEZuMwDC2" + }, + "source": [ + "# Qubit Routing" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "99c8d5767d47" + }, + "source": [ + "\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + " View on QuantumAI\n", + " \n", + " Run in Google Colab\n", + " \n", + " View source on GitHub\n", + " \n", + " Download notebook\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LprWZc4awbQ4" + }, + "source": [ + "In order to execute a circuit on quantum hardware, the logical qubits of a quantum circuit must be placed onto the physical qubits of the hardware device. Moreover, this placement is often not enough to guarantee that all 2-qubit operations between logical qubits in the ciruit correpond to 2-qubit operations between physical qubits on the device that are adjacent. So in addition to mapping logical qubits to physical qubits, it is often required to insert additional SWAP gates in the circuit to ensure that all 2-qubit operations are executed between physically adjacent qubits." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RrRN9ilV0Ltg" + }, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "EJcUPXkY0K93" + }, + "outputs": [], + "source": [ + "try:\n", + " import cirq\n", + "except ImportError:\n", + " print(\"installing cirq...\")\n", + " !pip install --quiet cirq --pre\n", + " import cirq\n", + "\n", + " print(\"installed cirq.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ot9EMHfg0175" + }, + "source": [ + "## Routing as a `@cirq.transfomer`" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8sVE5Mxw084B" + }, + "source": [ + "Routing in Cirq is implemented as a transformer class `RouteCQC`. An instance of `RouteCQC` is instantiated with a `nx.Graph` device graph. We will refer to this as the *router*. Calling this router on a `cirq.AbstractCircuit` circuit returns a *routed* `cirq.AbstractCircuit` circuit that is made of the device's physical qubits and contains only 2-qubit operations that are between physically adjacent qubits." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "UIEGpuvilO0l" + }, + "source": [ + "Before proceeding any further, we give a high-level overview of the algorithm implemented in `RouteCQC`. It is a heuristic that proceeds as follows:\n", + "\n", + "* Compute the **timesteps** of the circuit: considering operations in the given circuit from beginning to end, the next timestep is a maximal set of 2-qubit operations that act on disjoint qubits. It is 'maximal' because any 2-qubit gate's qubits in the next timestep must intersect with the qubits that are acted on in the current timestep.\n", + "* Place the logical qubits in the input circuit onto some input device graph by using an initial mapping strategy.\n", + "* Insert necessary swaps to ensure all 2-qubit gates are between adjacent qubits on the device graph by traversing the timesteps from left to right and for each timestep:\n", + " 1. Remove any single qubit gate and executable 2-qubit gate in the current timestep and add it to the output routed circuit.\n", + " 2. If there aren't any gates left in the current timestep, move on to the next.\n", + " 3. If there are gates remaining in the current timestep, consider a set of candidate swaps on them and rank them based on a **heuristic cost function**. Pick the swap that minimises the cost and use it to update our logical to physical mapping. Repeat from 1." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eG3-uP8PlTdl" + }, + "source": [ + "**Note**: All n-qubit operations in the given circuit are assumed to be decomposed into 1/2-qubit operations (our transformer raises an error otherwise)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "_1ePzhvl3xB_" + }, + "source": [ + "### A Simple Example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NafLVgAD34IL" + }, + "outputs": [], + "source": [ + "# The circuit to be routed\n", + "q = cirq.LineQubit.range(4)\n", + "circuit = cirq.Circuit([\n", + " cirq.Moment(cirq.CNOT(q[2], q[0]), cirq.CNOT(q[1], q[3])),\n", + " cirq.Moment(cirq.X(q[0]), cirq.CNOT(q[1], q[2])),\n", + " cirq.Moment(cirq.CNOT(q[0], q[1])),\n", + " cirq.Moment(cirq.H.on_each(q[1], q[2])),\n", + " cirq.Moment(cirq.CNOT(q[1], q[0]), cirq.CNOT(q[3], q[2])),\n", + "])\n", + "print(circuit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WxHe11Yu9kzb" + }, + "outputs": [], + "source": [ + "# Initialize the router with Sycamore device hardware\n", + "import cirq_google as cg\n", + "device = cg.Sycamore\n", + "device_graph = device.metadata.nx_graph\n", + "router = cirq.RouteCQC(device_graph)\n", + "\n", + "# Let's look at what the device architecture looks like\n", + "print(device)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NO1dyRbS9-1x" + }, + "outputs": [], + "source": [ + "routed_circuit = router(circuit)\n", + "print(routed_circuit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "3KXcAFvN-kTV" + }, + "outputs": [], + "source": [ + "# Compile the gates in routed_circuit to Sycamore gates (this is done because `validate_circuit` checks\n", + "# that all 2-qubit operations are physically adjacent AND that all gates are part of the device's gateset).\n", + "routed_circuit = cirq.optimize_for_target_gateset(routed_circuit, gateset = cg.SycamoreTargetGateset())\n", + "# Validate our circuit\n", + "device.validate_circuit(routed_circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mVgwDUyT_pli" + }, + "source": [ + "### Optional Arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gtSz7AZL4cQP" + }, + "source": [ + "The `__call__` method of a router takes several optional arguments:\n", + "1. `lookahead_radius: int`: a tunable argument that controls a convergence parameter for the heuristic cost function. It corresponds to the maximum number of succeeding timesteps the algorithm will consider for ranking candidate swaps with the cost cost function.\n", + "2. `tag_inserted_swaps: bool`: whether or not a `cirq.RoutingSwapTag` should be attached to inserted swap operations in order to distinguish inserted SWAP gates by the routing procedure from SWAP gates part of the input circuit.\n", + "3. `initial_mapper: Optional['cirq.AbstractInitialMapper']`: an initial mapping strategy (placement) of logical qubits in the circuit onto physical qubits on the device. If not provided, defaults to an instance of `cirq.LineInitialMapper`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "I6csX1EJmAEU" + }, + "source": [ + "Here is an example of routing the same circuit on the same device with non-default optional arguments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "DU8lHNiK4eCL" + }, + "outputs": [], + "source": [ + "# Use a hard-coded initial mapping strategy of logical to physical that places q0, q1, q2, q3 onto\n", + "# Grid(3, 5), Grid(3, 6), Grid(4, 5), Grid(4, 6), respectively\n", + "gq = cirq.GridQubit(3, 5)\n", + "hc_initial_mapper = cirq.HardCodedInitialMapper(\n", + " {q[0]: gq, q[1]: gq + (0, -1), q[2]: gq + (-1, 0), q[3]: gq + (-1, -1)}\n", + ")\n", + "print(hc_initial_mapper)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "RMGa1h2uoKui" + }, + "outputs": [], + "source": [ + "routed_circuit = router(circuit, lookahead_radius = 5, tag_inserted_swaps=True, initial_mapper=hc_initial_mapper)\n", + "print(routed_circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bbfue03H_2Lc" + }, + "source": [ + "### Unitary Equivalence" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l1RLuUE02-Qd" + }, + "source": [ + "It is often the case that the routing process will return a routed circuit that is unitarily equivalent to the input circuit but with the order of the qubits permuted. This is handled by calling the method of the router `route_circuit` with the input ciruit as an argument instead of calling the router. \n", + "\n", + "In addition to returning the routed circuit, `route_circuit` also returns the initial mapping of logical to physical qubits and the permutation of qubits caused by insertion of SWAP gates. For example, one way to recover the input circuit's unitary is to permute the routed circuit's qubits as in the example below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "R9ikw2Jz3HND" + }, + "outputs": [], + "source": [ + "# Example with appending a QubitPermutationGate to `routed_circuit` and permuting initial order of qubits.\n", + "routed_circuit, initial_mapping, swap_mapping = router.route_circuit(circuit, initial_mapper=hc_initial_mapper)\n", + "print(circuit)\n", + "print(routed_circuit)\n", + "\n", + "# The following line that asserts `circuit` and `routed_circuit` have the same unitary will raise an error if uncommented.\n", + "# cirq.testing.assert_allclose_up_to_global_phase(circuit.unitary(), routed_circuit.unitary(), atol=1e-8)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RJLcsOr3sw0c" + }, + "source": [ + "Below we show one way of reordering the qubits in `routed_circuit` using `initial_mapping` and `swap_map` to yield the same unitary as the original input circuit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "pIpTv5LXq_9m" + }, + "outputs": [], + "source": [ + "# Add a permutation gate that undoes the action of inserted SWAP gates\n", + "initial_qubits, sorted_qubits = zip(*sorted(swap_mapping.items(), key=lambda x: x[1]))\n", + "inverse_swap_permutation = [sorted_qubits.index(q) for q in initial_qubits]\n", + "routed_circuit.append(cirq.QubitPermutationGate(list(inverse_swap_permutation)).on(*sorted_qubits))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "HHRd-CmqsiQm" + }, + "outputs": [], + "source": [ + "# Reorder to initial physical qubits in `routed_circuit` based on their ordering in `circuit`\n", + "_, order = zip(*sorted(list(initial_mapping.items()), key=lambda x: x[0]))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "m_AyaXH8snjG" + }, + "outputs": [], + "source": [ + "print(circuit)\n", + "print(routed_circuit)\n", + "\n", + "# This will not raise an errors now\n", + "cirq.testing.assert_allclose_up_to_global_phase(circuit.unitary(), routed_circuit.unitary(qubit_order=order), atol=1e-8)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RccNfl947CyV" + }, + "source": [ + "**Note**: the decomposition of the `SwapPermutationGate` is a series of SWAP gates that may be applied on qubits that are not physically adjacent. \n", + "\n", + "In practice, the user would seldom need to undo the permutation due to inserted SWAP gates as they are often just doing some measurement at the end. Instead, this can be done correctly by just keeping track of the qubit with terminal measurement gate using *measurement keys* (which are unaffected by routing) or by looking at `initial_mapping` and `swap_mapping` to manually trace the permutation of the qubit in question." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DNmE3napAXzg" + }, + "source": [ + "## Extending the Routing API" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mvLpskslAdEa" + }, + "source": [ + "### By Overriding the Heuristic Cost Function" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tV6Ms1CVtpJv" + }, + "source": [ + "A user may decide that the existing cost function in `RouteCQC` can be improved or they may have a cost function that performs better in particular cases (injecting noise awareness, working devices with fixed topologies, etc.) We provide an easy to override the existing cost function by means of class extension. For example," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yqWstLnSuSw6" + }, + "outputs": [], + "source": [ + "from typing import Tuple, List, Any\n", + "\n", + "QidPair = QidPair = Tuple['cirq.Qid', 'cirq.Qid']\n", + "\n", + "class RouteCQCSimpleCostFunction(cirq.RouteCQC):\n", + " @classmethod\n", + " def _cost(\n", + " cls,\n", + " mm: cirq.MappingManager,\n", + " swaps: Tuple[QidPair, ...],\n", + " two_qubit_ops: List[cirq.Operation],\n", + " ) -> Any:\n", + " \"\"\"Computes the # of 2-qubit gates executable after applying SWAPs.\"\"\"\n", + " for swap in swaps:\n", + " mm.apply_swap(*swap)\n", + " ret = sum(1 for op in two_qubit_ops if mm.can_execute(op))\n", + " for swap in swaps:\n", + " mm.apply_swap(*swap)\n", + " return ret" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eea4dc233a27" + }, + "outputs": [], + "source": [ + "new_router = RouteCQCSimpleCostFunction(device_graph)\n", + "routed_circuit = new_router(circuit)\n", + "print(circuit)\n", + "print(routed_circuit)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l-MenrPoAgjO" + }, + "source": [ + "### By Defining a new Initial Mapping Strategy" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mpzD3iFlwSd4" + }, + "source": [ + "Similarly, any concrete class that implements the interface `cirq.AbstractInitialMapper` can be used as an optional argument to the `__call__` or `route_circuit` method in `RouteCQC`. \n", + "\n", + "The use cases for this are also similar to overriding the cost function. For example, placing logical qubits on a subset of the physical qubits that are well calibrated or taking advantage of certain device topologies to come up with a initial mapping strategy that yields a more highly connected image under the initial mapping." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "routing_transformer.ipynb", + "toc_visible": true + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}