Skip to content

Qiskit Interop

Ian Davis edited this page Oct 2, 2024 · 12 revisions

Qiskit Interop

The modern QDK provides interoperability with Qiskit circuits built upon the core Q# compiler infrastructure.

Leveraging the Q# compiler's capabilities for analysis, transformation, code generation, and simulation, the Qiskit interop provided by the QDK includes:

Detailed examples, potential errors, and usage with parameterized ciruits are demonstrated in the sample Qiskit interop notebook.

Installation

To get started, you'll need the qsharp and qiskit packages installed. As a convenience, you can install this via the qsharp package as an optional dependency. You'll also wand the Q# widgets for visualization:

pip install "qsharp[qiskit,widgets]==1.9"

Resource estimation

The resource estimation APIs have two primary ways to perform resource estimation

  • ResourceEstimatorBackend class
  • estimate function

The estimate function is a convenience wrapper which creates a ResourceEstimatorBackend, runs it, and returns the result.

For a detailed set of examples, see the resource estimation with Qiskit sample notebook. These examples also show how to use the qsharp-widgets for resource estimation in your own notebooks.

Azure Quantum Resource Estimation

The modern QDK's resource estimation capabilities are an easy drop-in replacement for users of the existing Azure Quantum Resource Estimator tools provided by the azure-quantum Python package.

Taking the following Azure Quantum example:

from qiskit.circuit.random import random_circuit
from azure.quantum import Workspace
from azure.quantum.qiskit import AzureQuantumProvider

workspace = Workspace(
    resource_id = "",
    location = "",
)

provider = AzureQuantumProvider(workspace)
backend = provider.get_backend('microsoft.estimator')
circuit = random_circuit(2, 2, measure=True)

result = backend.run(circuit).result()

To migrate to local estimation with the modern QDK, we'll replace azure.quantum.qiskit.AzureQuantumProvider with ResourceEstimatorBackend in qsharp.interop.qiskit:

from qiskit.circuit.random import random_circuit
from qsharp.interop.qiskit import ResourceEstimatorBackend

circuit = random_circuit(2, 2, measure=True)
backend = ResourceEstimatorBackend()

result = backend.run(circuit).result()

Q# simulation

The Qiskit interop is provided using OpenQASM 3. Qiskit cicuits are exported as OpenQASM which is parsed and compiled into a compatible format for Q#'s compiler. This enables running Qiskit circuits using the sparse simulator built into the qsharp package.

When compiling the OpenQASM, the output semantics are translated to Qiskit's expected ordering and endianess.

from qiskit.circuit.random import random_circuit
from qsharp.interop.qiskit import QSharpBackend

circuit = random_circuit(2, 2, measure=True)
backend = QSharpBackend()

job = backend.run(circuit)
counts = job.result().get_counts()

print(counts)

In addition to regular circuits, newer Qiskit support for classical instructions is supported. An example can be found below in the QIR generation section.

QIR generation

QIR generation, given that it is designed for running on hardware, imposes additional constraints on programs.

When generating QIR, all registers must have been measured into. If there are any unused registers, an error will be raised. Additionally, attempting to generate QIR when the profile is set to Unrestricted will raise an error. The Unrestricted profile is only valid for simulation. Either TargetProfile.Base or TargetProfile.Adaptive_RI must be used. The target_profile can be overridden in the backend.qir(...) call to switch profiles.

from qiskit import ClassicalRegister, QuantumRegister
from qiskit.circuit import (
    QuantumCircuit,
)

from qsharp import QSharpError, TargetProfile

qreg = QuantumRegister(3, name="q")
creg = ClassicalRegister(3, name="c")
qc = QuantumCircuit(qreg, creg)
qc.h([0, 1, 2])
qc.measure_all(add_bits=False)

with qc.switch(creg) as case:
    with case(7):
        qc.x(0)
    with case(1, 2):
        qc.z(1)
    with case(case.DEFAULT):
        qc.cx(0, 1)
qc.measure_all(add_bits=False)

backend = QSharpBackend()

print(backend.run(qc).result())

Using that same circuit, we can generate QIR which is used to run on quantum hardware.

# The default profile if not set is `TargetProfile.Unrestricted`
backend = QSharpBackend()
# here we override the default profile for the backend.
print(backend.qir(qc, target_profile=TargetProfile.Adaptive_RI))

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.

try:
    backend.qir(qc, target_profile=TargetProfile.Base)
except QSharpError as e:
    print(e)

Unsupported Qiskit features

Q# and Qiskit have differing semantics and targets, thus their features don't completely overlap. Below are some differences and unsupported capabilities:

  • Hardware qubit IDs
  • Timing and pulse control intrinsics
  • Mutable arrays
  • ClassicalRegister and ClBit can only hold qubit measurements and must be initialized with a measurement before use.