{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Quantinuum Systems - Launching circuits and retrieving results\n", "=================================================================" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this tutorial, we will demonstrate the process of initiating circuits on the backend and obtaining the corresponding result handles using Quantinuum Systems. To illustrate this, we will conduct a Hamiltonian averaging experiment for a 6-qubit active-space of the Methane chemical system.\n", "\n", "This tutorial requires that you have credentials for Quantinuum Systems, which can be obtained by contacting Quantinuum support [here](https://www.quantinuum.com/contact/quantinuum-systems). You will require credits (HQCs) to run on emulators and hardware. Users can access Quantinuum backends through [qnexus](https://docs.quantinuum.com/nexus/trainings/notebooks/basics/getting_started.html).\n", "\n", "The steps below are such:\n", "\n", "- Configuring the Quantinuum backend\n", "- Define the system\n", "- Find optimal VQE parameters on a noiseless simulator\n", "- Define protocol and build InQuanto computables\n", "- Submit circuits to backend\n", "- Retrieve results from backend\n", "- Evaluate the expectation value of the system" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The express module is used to load in the converged mean-field (Hartree-Fock) spin-orbitals, and Hamiltonian from a H$_2$ computation using the STO-3G basis set." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')\n", "\n", "from inquanto.express import load_h5\n", "\n", "h2 = load_h5(\"h2_sto3g.h5\", as_tuple=True)\n", "hamiltonian = h2.hamiltonian_operator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Fermionic space (`FermionSpace`) is defined with 4 spin-orbitals (which matches the full H2 STO-3G space) and the D2h point group is employed. This point group is the most practical high symmetry group to approximate the D(infinity)h group. We also explicitly define the orbital symmetries.\n", "\n", "The Fermionic state (`FermionState`) is then determined by the ground state occupations [1,1,0,0] and the Hamiltonian is encoded from the Hartree-fock integrals. The `qubit_encode` function carries out qubit encoding, utilizing the mapping class associated with the current integral operator. The default mapping approach is the Jordan-Wigner method." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from inquanto.spaces import FermionSpace\n", "from inquanto.states import FermionState\n", "from inquanto.symmetry import PointGroup\n", "\n", "space = FermionSpace(\n", " 4, point_group=PointGroup(\"D2h\"), orb_irreps=[\"Ag\", \"Ag\", \"B1u\", \"B1u\"]\n", ")\n", "\n", "state = FermionState([1, 1, 0, 0])\n", "qubit_hamiltonian = hamiltonian.qubit_encode()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To construct our ansatz for the specified fermion space and fermion state, we have employed the Chemically Aware Unitary Coupled Cluster method with singles and doubles excitations (UCCSD). The circuit is synthesized using Jordan-Wigner encoding." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from inquanto.ansatzes import FermionSpaceAnsatzChemicallyAwareUCCSD\n", "\n", "ansatz = FermionSpaceAnsatzChemicallyAwareUCCSD(space, state)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, we perform a simple experiment using a Variational Quantum Eigensolver (VQE) on a statevector backend to identify the optimal parameters that result in the ground state energy of our system. This enables us to carry out experiments on both quantum hardware and emulators using these pre-optimized parameters. For a more comprehensive guide on performing VQE calculations using InQuanto on quantum computers, we recommend referring to the [ VQE tutorial](InQ_tut_vqe_1.ipynb)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "# TIMER BLOCK-1 BEGINS AT 2025-12-15 23:48:31.247141\n", "# TIMER BLOCK-1 ENDS - DURATION (s): 0.1189818 [0:00:00.118982]\n" ] } ], "source": [ "from inquanto.express import run_vqe\n", "from pytket.extensions.qulacs import QulacsBackend\n", "\n", "state_backend = QulacsBackend()\n", "\n", "vqe = run_vqe(ansatz, hamiltonian, backend=state_backend, with_gradient=False)\n", "\n", "parameters = vqe.final_parameters" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "To reduce errors and inaccuracies caused by quantum noise and imperfections in the Quantinuum device, we can employ noise mitigation techniques. In this case, we will define the Qubit Operator symmetries within the system, enabling us to utilize [PMSV](https://arxiv.org/abs/2109.08401) (Partition Measurement Symmetry Verification). PMSV is an efficient technique for symmetry-verified quantum calculations. It represents molecular symmetries using Pauli strings, including mirror planes (Z2) and electron-number conservation (U1). For systems with Abelian point group symmetry, qubit tapering methods can be applied. PMSV uses commutation between Pauli symmetries and Hamiltonian terms for symmetry verification. It groups them into sets of commuting Pauli strings. If each string in a set commutes with the symmetry operator, measurement circuits for that set can be verified for symmetry without additional quantum resources, discarding measurements violating system point group symmetries.\n", "\n", "Parameters used:\n", "\n", "`stabilizers` – List of state stabilizers as QubitOperators with only single pauli strings in them.\n", "\n", "The InQuanto `symmetry_operators_z2_in_sector` function is employed to retrieve a list of symmetry operators applicable to our system. These symmetry operators are associated with the point group, spin parity, and particle number parity Z2 symmetries that uphold a specific symmetry sector. You can find additional details regarding this in the linked [page](https://docs.quantinuum.com/inquanto/api/inquanto/spaces.html#inquanto.spaces.FermionSpace.symmetry_operators_z2_in_sector)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from inquanto.protocols import PMSV\n", "from inquanto.mappings import QubitMappingJordanWigner\n", "\n", "stabilizers = QubitMappingJordanWigner().operator_map(\n", " space.symmetry_operators_z2_in_sector(state)\n", ")\n", "\n", "mitms_pmsv = PMSV(stabilizers)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To simulate the specific noise profiles of machines, we can instantiate an emulator of Quantinuum Systems via the `QuantinuumConfig`. The `QuantinuumConfig` offers a range of available emulators, such as H2-1E and H2-2E. These are device-specific emulators for the corresponding hardware devices. These emulators run remotely on a server." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from qnexus.client import auth, projects, devices\n", "from qnexus import QuantinuumConfig\n", "\n", "# Login with your credentials\n", "# You only need to login once per session, then this can be commented out\n", "# auth.login()\n", "\n", "# qnexus requires the specification of a project for all submitted jobs\n", "project_ref = projects.get_or_create(name=\"InQuanto Documentation\", description=\"\", properties={})\n", "\n", "# Change the machine name if necessary.\n", "machine = \"H2-1E\"\n", "backend = QuantinuumConfig(device_name=machine)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To compute the expectation value of a Hermitian operator through operator averaging on the system register, we employ the `PauliAveraging` protocol. This protocol effectively implements the procedure outlined in [Operator Averaging](https://docs.quantinuum.com/inquanto/manual/protocols/expval.html)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from inquanto.protocols import PauliAveraging\n", "from pytket.partition import PauliPartitionStrat\n", "\n", "\n", "protocol = PauliAveraging(\n", " backend,\n", " shots_per_circuit=5000,\n", " pauli_partition_strategy=PauliPartitionStrat.CommutingSets,\n", " project_ref=project_ref\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A protocol has been constructed to process a computable dataset and calculate the expected value." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\n", "protocol.build(parameters, ansatz, qubit_hamiltonian, noise_mitigation=mitms_pmsv)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# requires Quantinuum credentials\n", "protocol.compile_circuits()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also display a Pandas DataFrame using `dataframe_measurements` containing columns 'pauli_string,' 'mean,' and 'stderr.' Each row corresponds to a distinct Pauli string and its respective mean and standard error. Moreover, the `dataframe_circuit_shot` function generates a Pandas DataFrame containing circuit, shot, and depth details." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " Pauli string Mean Standard error Sample size\n", "0 Z0 None None None\n", "1 Z3 None None None\n", "2 X0 Y1 Y2 X3 None None None\n", "3 Y0 Y1 X2 X3 None None None\n", "4 Z2 None None None\n", "5 Z1 Z3 None None None\n", "6 X0 X1 Y2 Y3 None None None\n", "7 Z2 Z3 None None None\n", "8 Z0 Z2 None None None\n", "9 Z1 Z2 None None None\n", "10 Z1 None None None\n", "11 Z0 Z1 None None None\n", "12 Z0 Z3 None None None\n", "13 Y0 X1 X2 Y3 None None None\n", "\n", " Qubits Depth Count Depth2q Count2q DepthCX CountCX Shots\n", "0 4 11 23 5 6 0 0 5000\n", "1 4 8 18 3 4 0 0 5000\n", "Sum - - - - - - - 10000\n" ] } ], "source": [ "print(protocol.dataframe_measurements())\n", "print('')\n", "print(protocol.dataframe_circuit_shot())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `dumps` function allows you to pickle protocols for later reloading using `loads`. Additionally, you have the option to clear internal protocol data using `clear`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pickled_data = protocol.dumps()\n", "new_protocol = PauliAveraging.loads(pickled_data)\n", "\n", "protocol.clear()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Running an experiment involves launching the circuits to the backend using the `launch` function. This approach handles all the circuits related to the expectation value calculations and provides an `AsyncHandle` object. Alternatively, an experiment can be initiated by employing the `run` function, which automatically executes the launch and retrieve methods. Typically, the `run` method is more useful for statevector calculations where you will receive your results from the backend immediately. On the other hand, `launch` and `retrieve` are more suitable for situations in which you expect a delay in receiving the results.\n", "\n", "You could attempt both methods and print out the computational details to verify that you obtain the same results." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "handles = new_protocol.launch()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can pickle these ResultHandle objects so we can retrieve the results once there are ready." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pickle\n", "\n", "with open(\"handles.pickle\", \"wb\") as handle:\n", " pickle.dump(handles, handle, protocol=pickle.HIGHEST_PROTOCOL)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can monitor the progress of your experiments on [Quantinuum Nexus](https://nexus.quantinuum.com/) using the same credentials you used to run the experiments.\n", "\n", "After our experiments have finished, we can obtain the results by utilizing the `retrieve` function, which retrieves distributions from the backend for the specified source. The expectation value of a kernel for a specified quantum state is calculated by using the `evaluate_expectation_value` function. In addition, we have employed the `evaluate_expectation_uvalue` function, which calculates the expectation value of the Hermitian kernel while considering linear error propagation theory." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "with open(\"handles.pickle\", \"rb\") as handle:\n", " new_handles = pickle.load(handle)\n", "print(new_handles)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Below we evaluate the expectation value and its uncertainty due to noise and sampling." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Energy Value:\n", "-1.133093129368024\n", "Energy with error:\n", "-1.1331+/-0.0018\n" ] } ], "source": [ "\n", "new_protocol.retrieve(new_handles)\n", "\n", "energy_value = new_protocol.evaluate_expectation_value(ansatz, qubit_hamiltonian)\n", "print(\"Energy Value:\\n{}\".format(energy_value))\n", "\n", "error = new_protocol.evaluate_expectation_uvalue(ansatz, qubit_hamiltonian)\n", "print(\"Energy with error:\\n{}\".format(error))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": {}, "nbformat": 4, "nbformat_minor": 4 }