Skip to main content

Step 1. Setup: import Haiqu and initialize your session

We begin by importing the Haiqu SDK and some utility functions, along with Qiskit and Matplotlib.
# Standard setup: import Haiqu and log in
import qiskit
import json
import qiskit.circuit.library as qiskit_library
import matplotlib.pyplot as plt
from haiqu.sdk import haiqu

from utils.qft_sinusoids import *
To use the Haiqu SDK we need to log in and initialize a new Haiqu session, which we name “Getting started” — this will track our experiment and prepare the environment.
haiqu.login()
haiqu.init("Getting started")

Step 2. Bulding the first algorithm prototype

Let’s begin by putting together the first attempt at the quantum circuit implementing our data loading + QFT task, for the moment only using standard Qiskit tools (with which the Haiqu SDK is perfectly integrated). We choose the number of qubits to encode the sinusoidal signal, and generate a random mixture of sinusoids to use as input data.
num_qubits = 8
sinusoids = get_sinusoids(num_qubits=num_qubits, frequencies=[20, 80])

plt.plot(sinusoids)
plt.show()
To represent our mixture of sinusoids as a quantum state we need a quantum circuit that encodes the signal into the 2num_qubits amplitudes of the state. Qiskit provides a built-in StatePreparation routine, which automatically constructs a circuit preparing the desired state. Since this is our first prototype, let’s use it (though we will soon convince ourselves that this circuit requires prohibitively many quantum gates).
qiskit_sins_circuit = qiskit_library.StatePreparation(sinusoids)
We now combine the encoding and the QFT routine into a single circuit.
qiskit_circuit = qiskit.QuantumCircuit(num_qubits)
qiskit_circuit.append(qiskit_sins_circuit, range(num_qubits))
qiskit_circuit.append(qiskit_library.QFT(num_qubits), range(num_qubits))
qiskit_circuit.measure_all()

Step 3. Testing the prototype on an ideal simulator

Now that we’ve built our full circuit prototype, we can execute it using Haiqu. You can run circuits on real QPU hardware, hardware noise simulators, or ideal (noiseless) simulators. If possible, it is always a good practice to start by running on a noiseless simulator, to verify the expected frequency output.
ideal_results = haiqu.run(
    qiskit_circuit,
    shots=1000,
    backend_name="aer_simulator"
).result()[0]
The result of executing the QFT algorithm is the spectrum of the input signal. On an ideal simulator its plot should reveal a set of peaks corresponding to signal frequencies. Indeed, the plot below shows peaks at the two expected frequencies — the ones we encoded in the input:
plot_frequencies(ideal_results)

Step 4. Testing the prototype on a noisy simulator

So far, so good. But we’d like to run our algorithm on a quantum computer, which is inevitably noisy. Before executing on real QPU (which is costly!), let us test our prototype using a noisy simulator. This helps to estimate how well the circuit might perform under realistic conditions. Here, we use the fake_sherbrooke backend — a simulated model of an actual device — to evaluate our circuit’s robustness to noise.
qiskit_results = haiqu.run(
    qiskit_circuit,
    shots=1000,
    backend_name="fake_sherbrooke"
).result()[0]

plot_frequencies(qiskit_results)
Plotting the result of running the circuit on a noisy simulator we discover that the signal is extremely corrupted — the output is scattered, and we can no longer distinguish the original frequency peaks we encoded! If the result is poor on a noisy simulator, it will be even worse on real QPUs, since simulators use simplified noise models that do not capture all the complexities of device noise. It seems we have a problem on our hands… can we do something about it? Yes! Let’s see how Haiqu’s tools can help.

Step 5. Analyzing the prototype circuit with Haiqu tools

In order to improve the algorithm we first need to understand what the main culprit of the poor circuit performance in the presence of noise is. We can use Haiqu’s analysis tools to inspect the circuit:
# log circuit to run the analytics job at the backend
qiskit_meta = haiqu.log(qiskit_circuit)
# run blocking function that check if the analytics job is completed
qiskit_meta.wait_for_analytics()

# The Haiqu SDK provides a number of analytics functions, 
# e.g., rendering a widget that displays a table of core circuit analytic metrics.
qiskit_meta.core_metrics(help=True)
The analytics report contains a lot of information, but a basic metric reveals a problem — our prototype circuit uses more than 300 two-qubit gates, a number that is difficult to execute faithfully on the current noisy QPUs. To move forward, we will need to reduce the size and complexity of the circuit.

Step 6. Building a reduced circuit with Haiqu’s vector loading

Since our original circuit is too large — and signal encoding, i.e. preapring the input state for QFT, is a major contributor to its size — we can switch to using Haiqu’s vector loading, which generates a much more compact circuit to represent the same input state. This significantly reduces the number of gates while preserving high fidelity, as we shall see.
haiqu_sins_circuit, fidelity = haiqu.vector_loading(
    name="Sinusoids",
    data=sinusoids,
    num_layers=3,
).result()
print("State is loaded with fidelity:", fidelity)
Now we rebuild the circuit using the compact Haiqu vector-loaded state, followed by the QFT and final measurement.
haiqu_circuit = qiskit.QuantumCircuit(num_qubits)
haiqu_circuit.append(haiqu_sins_circuit, range(num_qubits))
haiqu_circuit.append(qiskit_library.QFT(num_qubits), range(num_qubits))
haiqu_circuit.measure_all()
Let us now run analytics on the improved circuit:
haiqu_meta = haiqu.log(haiqu_circuit)
haiqu_meta.wait_for_analytics()
haiqu_meta.core_metrics(help=True)
We observe that Haiqu’s vector loading helped reduce 2-qubit gate count by 3×. This should make the circuit much less affected by the QPU noise, and improve the algorithm results — let us verify this is indeed the case!

Step 7. Verifying the performance of the optimized circuit on a noisy simulator

Let’s now run the new, optimized circuit (reduced using vector loading) on the same noisy simulator with the same number of shots and compare the results.
haiqu_results = haiqu.run(
    haiqu_circuit,
    shots=1000,
    backend_name="fake_sherbrooke"
).result()[0]

plot_frequencies(haiqu_results)
Lo and behold, the correct frequency peaks are back! Though the background noise is visible, the main peaks are clearly identifaible, and in correct positions. This demonstrates that using a more efficient state preparation method such as Haiqu’s vector loading can significantly improve performance in the presence of noise, and be the difference between obtaining meaningful results and noise. How about trying a real QPU then?

Step 8. Setting up the physical QPU backend

Now that we’ve obtained satisfactory results on the noisy simulator, we can try running the same circuit on a real quantum processor (QPU) to see how it performs in practice. Here, we configure the backend using Haiqu’s support for IBM Quantum, specifying the target device and API credentials. For better privacy, it is possible to generate a temporary token for IBM Quantum locally so that you don’t haveneed to provide the permanent API key. For this use the get_ibmq_temporary_token function from Haiqu SDK. Note, that temporary token function is only available for jobs launched via IBM Quantum. Should you want to run your workloads via IBM Cloud, make sure to create an API key via the IBM Cloud Access(IAM) manager.
# from haiqu.sdk import get_ibmq_temporary_token

# API_KEY = "YOUR_API_KEY"
# TEMPORARY_TOKEN = get_ibmq_temporary_token(API_KEY)
With the temporary token let us set up a physical QPU backend:
# backend_options = {
#     "transpilation_options": {
#         "optimization_level": 3,
#     },
#     "platform": "ibm_quantum",
#     "api_key": TEMPORARY_TOKEN["token"],
# }

Step 9. Executing on a physical QPU: the first attempt

Let us submit our circuit for execution on a real QPU. To actually execute the circuit on the device, please uncomment the code block below. For convenience, we’ve included pre-run results from ibm_sherbrooke, so you can proceed directly to visualization and analysis.
# results_qpu = haiqu.run(
#     haiqu_circuit,
#     shots=1000,
#     backend_name="ibm_sherbrooke",
#     backend_options=backend_options,
# )

# results_qpu.retrieve_status()
# results_qpu.to_json("utils/qpu_results_ibm_sherbrooke_8q.json")
results_qpu = json.load(open("utils/qpu_results_ibm_sherbrooke_8q.json"))["results"][0]

plot_frequencies(results_qpu)
Although the improved circuit performed reasonably well on the noisy simulator, the noise on the physical QPU has completely destroyed the signal — we can no longer discern clear frequency peaks. Since we’ve already compressed the loading circuit significantly, and the QFT itself is shallow, there is limited room for further structural optimization. However, we still have one powerful tool left: Haiqu’s built-in error mitigation, which we can now apply to try and recover meaningful results on the noisy hardware.

Step 10. Executing on real QPU: using Haiqu’s error mitigation

To enable Haiqu’s error mitigation, simply add the use_mitigation=True option to your haiqu.run() call. As before, we loaded precomputed results to make visualization faster, but if you’d like to execute the circuit on the device yourself, simply uncomment the code block below.
# results_qpu_mitigated = haiqu.run(
#     haiqu_circuit,
#     shots=1000,
#     backend_name="ibm_sherbrooke",
#     backend_options=backend_options,
#     use_mitigation=True
# )

# results_qpu_mitigated.retrieve_status()
# results_qpu_mitigated.to_json("utils/qpu_results_mitigated_ibm_sherbrooke_8q.json")
results_qpu_mitigated = json.load(open("utils/qpu_results_mitigated_ibm_sherbrooke_8q.json"))["results"][0]

plot_frequencies(results_qpu_mitigated)
Compared to the first unmitigated attempt, execution with Haiqu’s error mitigation successfully recovered some of the key frequency peaks present in the data. While there is still a significant amount of noise, this result shows that mitigation can strongly improve the output quality, allowing to obtain meaningful results even on the noisy QPUs.

Next steps: where to go from here

The workflow we have just completed is just a glimpse of the full set of available tools. To learn more and explore other core features of the Haiqu SDK check the detailed notebooks listed below.
  • 🧪 Experiment Tracking
  • 📈 Circuit Analytics
  • ⚙️ Circuit Execution
A further set of examples and applications are available in the Run examples folder, arranged by increasing complexity. Specific examples relating to data loading are contained in the Data Loading examples folder.