{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Circuit Compilation\n", "\n", "This tutorial highlights circuit compilation in InQuanto. \n", "Circuit compilation is an important step in quantum computation. \n", "In this step measurement circuits are optimized, making them backend compatible and often cheaper to run. \n", "Compilation follows the instantiation and building of the protocol and precedes running the protocol and evaluating the results. \n", "In this tutorial we highlight the outcomes of different levels of compilation. \n", "\n", "We take advantage of IBM's Aer Backend through the `pytket-qiskit` extension in this tutorial. \n", "With some adjustment to how the protocols are run, this notebook can also take advantage of backends available through [Quantinuum Nexus](https://docs.quantinuum.com/nexus/index.html). \n", "Refer to this [page](https://docs.quantinuum.com/inquanto/tutorials/backends_overview.html#backends-overview-top) for instructions on how to run InQuanto calculations on other backends. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.1. System Preparation\n", "\n", "First, we import the Hamiltonian, space, and state of a converged mean-field (Hartree-Fock) simulation through the `get_system()` method of `inquanto.express`. Here we use H2 in the 6-31G basis set as our system." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from inquanto.express import get_system\n", "from inquanto.ansatzes import FermionSpaceAnsatzUCCSD\n", "\n", "fermion_hamiltonian, space, state = get_system(\"h2_631g_symmetry.h5\")\n", "# Jordan-Wigner encoding\n", "qubit_hamiltonian = fermion_hamiltonian.qubit_encode()\n", "ansatz = FermionSpaceAnsatzUCCSD(space, state)\n", "\n", "# Define a set of random variables for the ansatz\n", "params = ansatz.state_symbols.construct_random(seed=6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.2. Instancing and Building the Protocol\n", "\n", "A number of different [protocols](https://docs.quantinuum.com/inquanto/manual/protocols_overview.html) are available in InQuanto. \n", "There are state vector protocols, protocols for quantum phase estimation, and averaging protocols.\n", "We use the shot-based `PauliAveraging()` protocol, which can partition the Pauli gates that measure the Hamiltonian into sets that are simultaneously measurable. \n", "More on the Pauli averaging protocol can be found [here](https://docs.quantinuum.com/inquanto/manual/protocols/expval.html#pauliaveraging). \n", "In this tutorial we build and then [pickle](https://docs.python.org/3/library/pickle.html) a template of this protocol. \n", "This allows us to use a clean instance of the protocol each time we demonstrate different compilations." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pytket.extensions.qiskit import AerBackend\n", "from inquanto.protocols import PauliAveraging\n", "from pytket.partition import PauliPartitionStrat\n", "\n", "# Shot based IBM backend\n", "backend = AerBackend()\n", "\n", "# Instantiate protocol\n", "protocol_template = PauliAveraging(\n", " backend,\n", " shots_per_circuit=10,\n", " pauli_partition_strategy=PauliPartitionStrat.CommutingSets\n", ")\n", "# Build protocol\n", "protocol_template.build(params, ansatz, qubit_hamiltonian)\n", "protocol_pickle = protocol_template.dumps()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1.3. Circuit Compilation\n", "\n", "We are now able to begin compiling circuits with the instantiated and built protocol. \n", "If we inspect a circuit from the built protocol we find that it is comprised of two Pauli X gates, a number of circuit boxes and then a measurement circuit. \n", "The circuit boxes are abstractions built from sets of gates that pytket uses to exploit any higher-level structure within the circuit. \n", "These circuit boxes are themselves comprised of Pauli exponential circuit boxes. \n", "While all of these boxes can be useful to break large and complex circuits down into manageable sub-routines, they are not typically compatible circuit execution. \n", "We can check the validity of our sample circuit with the Aer backend using the `valid_circuit()` method from pytket, and see that the circuits in their current form are not compatible. \n", "However, this will change once they are compiled. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Circuit is compatible with AER backend: False\n" ] } ], "source": [ "from pytket.circuit.display import render_circuit_jupyter\n", "\n", "# Note that the PauliAveraging protocol builds a number of destinct circuits,\n", "# here we extract one from the set to examine. (The total number can be examined with protocol.n_circuit) \n", "protocol = PauliAveraging.loads(protocol_pickle, backend)\n", "example_circuit = protocol.get_circuits()[0]\n", "render_circuit_jupyter(example_circuit)\n", "print(f\"Circuit is compatible with AER backend: {backend.valid_circuit(example_circuit)}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "InQuanto pools all circuit compilation into the`compile_circuits()` method of `inquanto.protocols`. \n", "This compilation is handled by pytket.\n", "pytket-extensions detail a set default compilation passes for each architecture. These can be utilized using the `optimization_level` argument. \n", "This can take on three values: 0, 1, and 2. The classical computational expense of the applied passes, and the quality of improvement, is expected to scale up as this integer grows.\n", "These default passes are also backend specific, ensuring that the final circuit is executable on the particular architecture and in the gateset of the particular backend.\n", "\n", "More information on which compilation passes are applied at each optimization level for each backend can be found in the [pytket extensions documentation](https://docs.quantinuum.com/tket/api-docs/extensions.html). \n", "Pytket extensions seperate out the available backends by provider, relevant information for the IBM backend used in this tutorial is on the [pytket-qiskit](https://docs.quantinuum.com/tket/extensions/pytket-qiskit/#default-compilation) extension page.\n", "\n", "By inspecting the circuit, we can see that it has changed quite drastically. \n", "It is now compatible with the Aer backend and all the circuit boxes have been decomposed into gates. \n", "One such gate, that may be unfamiliar to the user is the TK1 gate (grey) which is an arbitrary single qubit rotation gate native to TKET. \n", "At this point, we can also start to get an idea of the resources required to execute these circuits on quantum backends. \n", "We use the controlled Pauli X (CX) gate as a proxy for the noise in executing the circuit. \n", "At `optimization_level=0` one of the compiled circuits contains 123 CX gates. (Note that the exact numbers may change with pytket versioning and improvements to compilation passes)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Circuit is compatible with AER backend: True\n", "Number of circuits = 11\n", "Optimization_level = 0 CNOT gates: 123\n" ] } ], "source": [ "from pytket import OpType\n", "\n", "# Clears the protocol object of any previous instance.\n", "protocol.clear() \n", "# Load clean protocol from pickle object\n", "protocol = PauliAveraging.loads(protocol_pickle, backend) \n", "protocol.compile_circuits(optimization_level=0)\n", "opt0_circuit = protocol.get_circuits()[0]\n", "\n", "render_circuit_jupyter(opt0_circuit)\n", "print(f\"Circuit is compatible with AER backend: {backend.valid_circuit(opt0_circuit)}\")\n", "print(f\"Number of circuits = {protocol.n_circuit}\")\n", "# Note: this print statement is for only one of the 11 compiled circuits\n", "# Two-qubit gate count as a proxy for requred resources\n", "cnot_gates = protocol.get_circuits()[0].n_gates_of_type(OpType.CX)\n", "print(f\"Optimization_level = 0 CNOT gates: {cnot_gates}\") \n", "\n", "# Creates a pickled bytes object of the compiled \n", "# protocol for measuring later in the notebook.\n", "opt0_protocol = protocol.dumps() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we increase the `optimization_level`, we see that the required resources decrease from 123 CX gates down to 97.\n", "The types of gates that appear in the circuit also changes.\n", "There are now only CX gates and the TK1 gates native to TKET mentioned previously." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Optimization_level = 2 CNOT gates: 97\n" ] } ], "source": [ "protocol.clear()\n", "protocol = PauliAveraging.loads(protocol_pickle, backend)\n", "protocol.compile_circuits(optimization_level=2)\n", "render_circuit_jupyter(protocol.get_circuits()[0])\n", "cnot_gates = protocol.get_circuits()[0].n_gates_of_type(OpType.CX)\n", "print(f\"Optimization_level = 2 CNOT gates: {cnot_gates}\") \n", "\n", "opt2_protocol = protocol.dumps()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On top of the default `optimization_level` passes that are applied, we can also specify passes to be applied before and/or after the default `pytket` optimizations. \n", "The `pytket.passes` [module](https://tket.quantinuum.com/api-docs/passes.html) provides a large number of compilation passes to experiment with. \n", "Some of these passes impose predicates on the circuit that they are applied to and often they take optional arguments, allowing further customization. \n", "\n", "The user can outline compilation passes to be applied before the default `optimization_level` passes using the `preoptimize_passes` keyword argument.\n", "Here we demonstrate the pytket `SequencePass()` method to apply a combination of `DecomposeBoxes()` and then `GreedyPauliSimp()` passses before the default optimization. \n", "We can also apply passes after the default optimization using the `compiler_passes` keyword argument. \n", "This is demonstrated with the `SynthesiseTK()` pass. \n", "It is important to note that passing `compiler_passes` sets `optimization_level=0`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Pre-optimization with GreedyPauliSimp() CNOT gates: 79\n" ] }, { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n", " \n", "
\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from pytket import passes\n", "\n", "protocol.clear()\n", "protocol = PauliAveraging.loads(protocol_pickle, backend)\n", "# strict=False required to apply GreedyPauliSimp().\n", "\n", "seq = passes.SequencePass([passes.DecomposeBoxes(), passes.GreedyPauliSimp()], strict=False)\n", "\n", "protocol.compile_circuits(preoptimize_passes=seq) \n", "render_circuit_jupyter(protocol.get_circuits()[0])\n", "cnot_gates = protocol.get_circuits()[0].n_gates_of_type(OpType.CX)\n", "print(f\"Pre-optimization with GreedyPauliSimp() CNOT gates: {cnot_gates}\") \n", "\n", "protocol.clear()\n", "protocol = PauliAveraging.loads(protocol_pickle, backend)\n", "protocol.compile_circuits(compiler_passes=passes.SynthesiseTK())\n", "render_circuit_jupyter(protocol.get_circuits()[0])\n", "tk2_gates = protocol.get_circuits()[0].n_gates_of_type(OpType.TK2)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Compilation is an important step in any quantum computational chemistry workflow as it ultimately determines the resources required to run circuits.\n", "Here we have demonstrated how a user can compile circuits using default optimisation as well as defining their own sets of compilation passes." ] } ], "metadata": { "kernelspec": { "display_name": "inquanto-docs (3.12.3)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }