{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "ae56fce0",
      "metadata": {},
      "source": [
        "# QDK Interop with Qiskit"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2b838c23",
      "metadata": {},
      "source": [
        "The QDK provides interoperability with Qiskit circuits built upon the core QDK compiler infrastructure.\n",
        "\n",
        "This core enables integration and local resource estimation without relying on external tools. Users are able to estimate resources for their Qiskit circuits locally (see the [resource estimation with Qiskit sample notebook](../../estimation/estimation-qiskit.ipynb)), leveraging the Q# compiler's capabilities for analysis, transformation, code generation, and simulation. This also enables the generation of QIR from Qiskit circuits leveraging the [QDKs advanced code generation capabilities](https://devblogs.microsoft.com/qsharp/integrated-hybrid-support-in-the-azure-quantum-development-kit/).\n",
        "\n",
        "This includes support for circuits with classical instructions available in Qiskit such as for loops, if statements, switch statements, while loops, binary expresssions, and more."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4f761649",
      "metadata": {},
      "source": [
        "## Running Qiskit circuits\n",
        "The `QSharpBackend` backend is the main class to interact with for running circuits and generating QIR.\n",
        "\n",
        "To start, we'll set up a simple circuit with a prepared state."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e91dd2d7",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit import QuantumCircuit\n",
        "import numpy as np\n",
        "\n",
        "circuit = QuantumCircuit(2, 2)\n",
        "circuit.name = \"state_prep\"\n",
        "\n",
        "# State vector to initialize: |ψ⟩ = (|0⟩ - |1⟩) / √2\n",
        "circuit.initialize([1 / np.sqrt(2), -1 / np.sqrt(2)], 0)\n",
        "circuit.h(0)\n",
        "circuit.measure(0, 0)\n",
        "\n",
        "circuit.prepare_state([1 / np.sqrt(2), -1 / np.sqrt(2)], 1)\n",
        "circuit.h(1)\n",
        "circuit.measure(1, 1)\n",
        "\n",
        "circuit.draw(output=\"text\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "922f8420",
      "metadata": {},
      "source": [
        "With the circuit created, we can run the circuit with Q#'s backend. By default, it will use the `Unrestricted` profile meaning anything is allowed for simulation."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "7c69aeac",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qdk.qiskit import QSharpBackend\n",
        "\n",
        "backend = QSharpBackend()\n",
        "job = backend.run(circuit)\n",
        "counts = job.result().get_counts()\n",
        "print(counts)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9c84df75",
      "metadata": {},
      "source": [
        "## Parameterized Qiskit circuits\n",
        "\n",
        "Some circuits require parameters as input. To start, we'll define utility functions to create parameterized circuit(s)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5d766661",
      "metadata": {},
      "outputs": [],
      "source": [
        "from typing import List\n",
        "\n",
        "import numpy as np\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.circuit import Parameter\n",
        "\n",
        "\n",
        "def get_theta_range(samples: int) -> List[float]:\n",
        "    return np.linspace(0, 2 * np.pi, samples)\n",
        "\n",
        "\n",
        "def get_parameterized_circuit(n: int) -> QuantumCircuit:\n",
        "    theta = Parameter(\"θ\")\n",
        "    n = 5\n",
        "    qc = QuantumCircuit(n, 1)\n",
        "    qc.h(0)\n",
        "    for i in range(n - 1):\n",
        "        qc.cx(i, i + 1)\n",
        "    qc.barrier()\n",
        "    qc.rz(theta, range(n))\n",
        "    qc.barrier()\n",
        "\n",
        "    for i in reversed(range(n - 1)):\n",
        "        qc.cx(i, i + 1)\n",
        "    qc.h(0)\n",
        "    qc.measure(0, 0)\n",
        "    return qc\n",
        "\n",
        "\n",
        "def get_parameterized_circuits(n: int, theta_range: List[float]) -> List[QuantumCircuit]:\n",
        "    qc = get_parameterized_circuit(n)\n",
        "    qc.draw()\n",
        "    theta = qc.parameters[0]\n",
        "    circuits = [qc.assign_parameters({theta: theta_val}) for theta_val in theta_range]\n",
        "    return circuits"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d6b97850",
      "metadata": {},
      "source": [
        "Attempting to run without binding all input will generate an error in the job."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "4e05b81e",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qdk.qsharp import QSharpError, TargetProfile\n",
        "from qdk.qiskit import QSharpBackend\n",
        "\n",
        "circuit = get_parameterized_circuit(3)\n",
        "backend = QSharpBackend()\n",
        "try:\n",
        "    backend.qir(circuit, target_profile=TargetProfile.Base)\n",
        "except QSharpError as e:\n",
        "    print(e)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ee4d57b4",
      "metadata": {},
      "source": [
        "Any parameters must be bound before we can run the circuit. As we can see from the exception output, we must define the value for the input parameter `θ`. To do this, set the `params` argument to the `run` function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "d9bb3eb0",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qdk.qiskit import QSharpBackend\n",
        "\n",
        "circuit = get_parameterized_circuit(3)\n",
        "backend = QSharpBackend()\n",
        "\n",
        "circuit.assign_parameters(\n",
        "    {\"θ\": 0.5},\n",
        "    inplace=True,\n",
        ")\n",
        "job = backend.run(circuit)\n",
        "counts = job.result().get_counts()\n",
        "print(counts)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "974b3131",
      "metadata": {},
      "source": [
        "## Classical instructions in circuits"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "42f916c9",
      "metadata": {},
      "source": [
        "### Run Qiskit with classical instructions\n",
        "Qiskit has begun implementing some classical computation support as they expand their OpenQASM 3 support. These constructs, insofar as Qiskit can export them, can be consumed by Q#.\n",
        "\n",
        "As an example, we can create a classical switch statement in Qiskit and run the program."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "30d29199",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit import ClassicalRegister, QuantumRegister\n",
        "from qiskit.circuit import (\n",
        "    QuantumCircuit,\n",
        ")\n",
        "\n",
        "from qdk.qsharp import QSharpError, TargetProfile\n",
        "\n",
        "qreg = QuantumRegister(3, name=\"q\")\n",
        "creg = ClassicalRegister(3, name=\"c\")\n",
        "qc = QuantumCircuit(qreg, creg)\n",
        "qc.h([0, 1, 2])\n",
        "qc.measure_all(add_bits=False)\n",
        "\n",
        "with qc.switch(creg) as case:\n",
        "    with case(7):\n",
        "        qc.x(0)\n",
        "    with case(1, 2):\n",
        "        qc.z(1)\n",
        "    with case(case.DEFAULT):\n",
        "        qc.cx(0, 1)\n",
        "qc.measure_all(add_bits=False)\n",
        "\n",
        "backend = QSharpBackend()\n",
        "\n",
        "print(backend.run(qc).result())"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fea9051a",
      "metadata": {},
      "source": [
        "Using that same circuit, we can generate QIR which is used to run on quantum hardware."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0081f6f0",
      "metadata": {},
      "outputs": [],
      "source": [
        "backend = QSharpBackend(target_profile=TargetProfile.Adaptive_RI)\n",
        "print(backend.qir(qc))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "97184460",
      "metadata": {},
      "source": [
        "Not all programs can run on all hardware. Here we can try to target the `Base` profile, but we will get detailed errors on which parts of the program aren't supported."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "06c31a67",
      "metadata": {},
      "outputs": [],
      "source": [
        "try:\n",
        "    backend.qir(qc, target_profile=TargetProfile.Base)\n",
        "except QSharpError as e:\n",
        "    print(e)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b06b7857",
      "metadata": {},
      "source": [
        "## Errors\n",
        "#### Unsupported language features, `QasmError`, and `QSharpError`\n",
        "The QDK's interop with Qiskit is based on Qiskit's OpenQASM 3 support. Qiskit supports a subset of OpenQASM 3 features which may cause issues during conversion.\n",
        "\n",
        "If the Qiskit OpenQASM `Exporter` or OpenQASM parser don't support the feature yet, a `QasmError` is raised prior to conversion. When an OpenQASM parsing failure occurs, this is likely an issue with the Qiskit libraries parsing and/or export functionality. Additionally, failure to transform the OpenQASM into Q#'s internal representation will throw a `QasmError`. This is most likely due to a semantically invalid OpenQASM program as input or an unsupported language feauture is being used.\n",
        "\n",
        "If the program can't be compiled to QIR, has invalid input bindings, or encounters a runtime error, a `QSharpError` is raised.\n",
        "\n",
        "If the backend configuration is not valid for a given operation, a `ValueError` may be raised. This is most likely caused by trying to generate QIR with the `Unrestricted` profile."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d0f21006",
      "metadata": {},
      "source": [
        "### Semantic Errors\n",
        "It is still possible to create circuits that are semantically invalid. These will raise `QasmErrors` as the OpenQASM can't be compiled.\n",
        "\n",
        "\n",
        "We'll look at examples of each scenario.\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6ebacf90",
      "metadata": {},
      "source": [
        "#### General semantic errors\n",
        "Most common semantic errors arise from unsupported features:\n",
        "- No classical registers defined. If the circuit(s) being used do not measure into classical registers then the circuit is purely classical.\n",
        "- Aliases were used for classical registers.\n",
        "\n",
        "Example, creating a circuit without any output:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "b245c46c",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qdk.qiskit import QasmError\n",
        "\n",
        "try:\n",
        "    circuit = QuantumCircuit(2)\n",
        "    circuit.x(0)\n",
        "    backend = QSharpBackend()\n",
        "    print(backend.run(circuit).result())\n",
        "except QasmError as ex:\n",
        "    print(ex)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f2da9168",
      "metadata": {},
      "source": [
        "Example, using aliased classical registers:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "33fd3c4d",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qdk.qiskit import QasmError\n",
        "\n",
        "try:\n",
        "    q = QuantumRegister(2, name=\"q\")\n",
        "    cr1 = ClassicalRegister(1, name=\"cr1\")\n",
        "    cr2 = ClassicalRegister(1, name=\"cr2\")\n",
        "    # Create a ClassicalRegister with bits from two different QuantumRegisters\n",
        "    # which is not supported by the Q# backend.\n",
        "    cr3 = ClassicalRegister(\n",
        "        name=\"cr3\",\n",
        "        bits=[\n",
        "            cr1[0],\n",
        "            cr2[0],\n",
        "        ],\n",
        "    )\n",
        "    qc = QuantumCircuit(q, cr1, cr2, cr3)\n",
        "\n",
        "    backend = QSharpBackend(target_profile=TargetProfile.Base)\n",
        "    backend.qir(qc)\n",
        "except QasmError as ex:\n",
        "    print(f\"Exception: {str(ex)}\")\n",
        "    # Print the cause of the exception if it exists.\n",
        "    # This will print the error message from Qiskit itself.\n",
        "    if ex.__cause__:\n",
        "        print(f\"Cause: {str(ex.__cause__)}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "62e33a40",
      "metadata": {},
      "source": [
        "#### QIR generation semantic errors\n",
        "\n",
        "When targetting harware by compiling to QIR there are additional restrictions which may cause compilation errors. Most common scenarios:\n",
        "- Trying to generate QIR when the profile is set to `Unrestricted`. `Unrestricted` is only valid for simulation. Either `TargetProfile.Base` or `TargetProfile.Adaptive_RI` must be used.\n",
        "- Not all bits in classical registers have been assigned to. Usually because there were no measurements, or extra registers were declared.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ac63ad88",
      "metadata": {},
      "source": [
        "Example, generating QIR with `Unrestricted`"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "10d7dcb0",
      "metadata": {},
      "outputs": [],
      "source": [
        "try:\n",
        "    circuit = QuantumCircuit(1)\n",
        "    circuit.x(0)\n",
        "    circuit.measure_all()\n",
        "    backend = QSharpBackend()\n",
        "    print(backend.qir(circuit))\n",
        "except ValueError as ex:\n",
        "    print(ex)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fa219de1",
      "metadata": {},
      "source": [
        "To avoid this issue, set the `target_profile` argument either in the `QSharpBackend` creation or in the `backend.qir` call."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3170569e",
      "metadata": {},
      "source": [
        "When generating `QIR`, all output registers must be read into before generating QIR. Failure to do so results in a `QSharpError`.\n",
        "\n",
        "In this next example, we declare two output bits, but only measure into one. This causes an error because result values can only be a side effect of measurement, and cannot be used like classical variables when compiling for hardware."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "071480db",
      "metadata": {},
      "outputs": [],
      "source": [
        "circuit = QuantumCircuit(2, 2)\n",
        "circuit.x(0)\n",
        "circuit.measure(0, 1)\n",
        "backend = QSharpBackend(target_profile=TargetProfile.Base)\n",
        "try:\n",
        "    print(backend.qir(circuit))\n",
        "except QSharpError as ex:\n",
        "    print(ex)"
      ]
    }
  ],
  "metadata": {
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
